/*
 * <p>Title: ProView</p>
 * <p>Description: </p>
 * <p>Copyright: Copyright (c) 2007</p>
 * <p>Company: Institut de recherches cliniques de Montréal (IRCM)</p>
 */
package ca.qc.ircm.stripes.interceptor;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionEvent;

import ca.qc.ircm.stripes.annotation.Session;

import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.action.Resolution;
import net.sourceforge.stripes.controller.ExecutionContext;
import net.sourceforge.stripes.controller.Interceptor;
import net.sourceforge.stripes.controller.Intercepts;
import net.sourceforge.stripes.controller.LifecycleStage;
import net.sourceforge.stripes.util.ReflectUtil;

/**
 * Interceptor that stores or restores session objects.
 * 
 * @author poitrac
 */
@Intercepts(value={LifecycleStage.ActionBeanResolution, LifecycleStage.ResolutionExecution})
public class SessionStoreInterceptor implements Interceptor {
    
    /** Lazily filled in map of Class to fields annotated with Session. */
    private static Map<Class<?>, Collection<Field>> fieldMap = new ConcurrentHashMap<Class<?>, Collection<Field>>();
    
    /* (non-Javadoc)
     * @see net.sourceforge.stripes.controller.Interceptor#intercept(net.sourceforge.stripes.controller.ExecutionContext)
     */
    public Resolution intercept(ExecutionContext context) throws Exception {
        // Continue on and execute other filters and the lifecycle code.
        Resolution resolution = context.proceed();
        
        // Get all fields with session.
        Collection<Field> fields = getSessionFields(context.getActionBean().getClass());
        
        // Restores values from session.
        if (LifecycleStage.ActionBeanResolution.equals(context.getLifecycleStage())) {
            this.restoreFields(fields, context.getActionBean(), context.getActionBeanContext().getRequest().getSession());
        }
        // Store values in session.
        if (LifecycleStage.ResolutionExecution.equals(context.getLifecycleStage())) {
            this.saveFields(fields, context.getActionBean(), context.getActionBeanContext().getRequest().getSession());
        }
        
        return resolution;
    }
    
    /**
     * Saves all fields in session.
     * @param fields Fields to save in session.
     * @param actionBean ActionBean.
     * @param context WebContext.
     * @throws IllegalAccessException Cannot get access to some fields.
     */
    protected void saveFields(Collection<Field> fields, ActionBean actionBean, HttpSession session) throws IllegalAccessException {
        for (Field field : fields) {
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }
            set(session, getFieldKey(field), field.get(actionBean), ((Session)field.getAnnotation(Session.class)).serializable(), ((Session)field.getAnnotation(Session.class)).maxTime());
        }
    }
    /**
     * Restore all fields from value stored in session.
     * @param fields Fields to restore from session.
     * @param actionBean ActionBean.
     * @param session Session.
     * @throws IllegalAccessException Cannot get access to some fields.
     */
    protected void restoreFields(Collection<Field> fields, ActionBean actionBean, HttpSession session) throws IllegalAccessException {
        for (Field field : fields) {
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }
            Object value = get(session, getFieldKey(field));
            // If value is null and field is primitive, don't set value.
            if (value != null && !field.getType().isPrimitive()) {
                field.set(actionBean, value);
            }
        }
    }
    /**
     * Returns session key under which field should be saved or read.
     * @param field Field.
     * @return Session key under which field should be saved or read.
     */
    protected String getFieldKey(Field field) {
        return field.getDeclaringClass() + "#" + field.getName();
    }
    /**
     * Returns all fields with Session annotation for a class.
     * @param clazz Class.
     * @return All fields with Session annotation for a class.
     */
    protected static Collection<Field> getSessionFields(Class<?> clazz) {
        Collection<Field> fields = fieldMap.get(clazz);
        if (fields == null) {
            fields = ReflectUtil.getFields(clazz);
            Iterator<Field> iterator = fields.iterator();
            while (iterator.hasNext()) {
                Field field = iterator.next();
                if (!field.isAnnotationPresent(Session.class)) {
                    iterator.remove();
                }
            }
            fieldMap.put(clazz, fields);
        }
        return fields;
    }
    
    
    /**
     * Returns an object in session.
     * @param key Key under which object is saved.
     * @returns Object.
     */
    public Object get(HttpSession session, String key) {
        Object o = session.getAttribute(key);
        if (o instanceof MaxTimeSaver) {
            return ((MaxTimeSaver)o).o;
        }
        else {
            return o;
        }
    }
    /**
     * Saves an object in session for latter use.
     * @param session Session in which to store object.
     * @param key Key under which object is saved.
     * @param object Object to save.
     * @param serializable True if object is serializable.
     * @param maxTime Maximum time to keep object in session.
     * @returns Object previously saved under key.
     */
    public Object set(HttpSession session, String key, Object object, boolean serializable, int maxTime) {
        if (serializable) {
            Object ret = session.getAttribute(key);
            session.setAttribute(key, new MaxTimeSaver(object, maxTime));
            return ret;
        }
        else {
            Object ret = session.getAttribute(key);
            session.setAttribute(key, new NoSerializeSaver(object, maxTime));
            return ret;
        }
    }
    
    
    /**
     * Used to store non-serializable objects into session.
     * @author poitrac
     */
    private class NoSerializeSaver extends MaxTimeSaver implements HttpSessionActivationListener {
        /**
         * Creates a NoSerializeSaver with no maximum time.
         * @param o Object to store.
         */
        NoSerializeSaver(Object o) {
            super(o, -1);
        }
        /**
         * Creates a NoSerializeSaver with maximum time.
         * @param o Object to store.
         */
        NoSerializeSaver(Object o, int maxTime) {
            super(o, maxTime);
        }
        public void sessionDidActivate(HttpSessionEvent event) {
        }
        /**
         * Remove object from session to prevent serialization.
         */
        public void sessionWillPassivate(HttpSessionEvent event) {
            event.getSession().removeAttribute(key);
        }
    }
    /**
     * Used to store object in session for a maximum time.
     * @author poitrac
     */
    private class MaxTimeSaver implements HttpSessionBindingListener {
        /**
         * Key under which object is bound.
         */
        String key;
        /**
         * Bounded object.
         */
        Object o;
        /**
         * Maximum time in session.
         */
        int maxTime;
        /**
         * Session where object is stored.
         */
        HttpSession session;
        /**
         * Deleter thread.
         */
        Deleter deleter;
        /**
         * Creates a MaxTimeSaver.
         * @param o Object to store.
         * @param maxTime Maximum number of minutes in session.
         */
        MaxTimeSaver(Object o, int maxTime) {
            this.o = o;
            this.maxTime = maxTime;
        }
        /**
         * Start deleter thread.
         */
        public void valueBound(HttpSessionBindingEvent event) {
            key = event.getName();
            session = event.getSession();
            deleter = new Deleter();
            Thread t = new Thread(deleter);
            t.start();
        }
        /**
         * Stop deleter thread.
         */
        public void valueUnbound(HttpSessionBindingEvent event) {
            deleter.cancel();
            deleter = null;
        }
        /**
         * Remove an object in session after a certain time.
         * @author poitrac
         */
        private class Deleter implements Runnable {
            private Date endTime;
            private boolean cancel;
            public void run() {
                if (maxTime == -1) {
                    return;
                }
                long timeMillis = (long)maxTime * 60 * 1000;
                endTime = new Date((new Date()).getTime() + timeMillis);
                while (!cancel && endTime.after(new Date())) {
                    try {
                        Thread.sleep(1000);
                    }
                    catch (InterruptedException e) {
                    }
                }
                if (!cancel && !endTime.after(new Date())) {
                    session.removeAttribute(key);
                }
            }
            protected void cancel() {
                cancel = true;
            }
        }
    }
}
