/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.myfaces.orchestra.conversation.servlet;

import java.util.Enumeration;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.myfaces.orchestra.conversation.ConversationManager;
import org.apache.myfaces.orchestra.conversation.ConversationWiperThread;
import org.apache.myfaces.orchestra.conversation.ConversationMessager;
import org.apache.myfaces.orchestra.conversation.basic.LogConversationMessager;
import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
import org.apache.myfaces.orchestra.frameworkAdapter.local.LocalFrameworkAdapter;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

/**
 * An http session listener which periodically scans every http session for
 * conversations and conversation contexts that have exceeded their timeout.
 * <p>
 * If a web application wants to configure a conversation timeout that is
 * shorter than the http session timeout, then this class must be specified
 * as a listener in the web.xml file.
 * <p>
 * A conversation timeout is useful because the session timeout is refreshed
 * every time a request is made. If a user starts a conversation that uses
 * lots of memory, then abandons it and starts working elsewhere in the same
 * webapp then the session will continue to live, and therefore so will that
 * old "unused" conversation. Specifying a conversation timeout allows the
 * memory for that conversation to be reclaimed in this situation.
 * <p>
 * This listener starts a single background thread that periodically wakes
 * up and scans all http sessions to find ConversationContext objects, and
 * checks their timeout together with the timeout for all Conversations in
 * that context. If a conversation or context timeout has expired then it
 * is removed.
 * <p>
 * This code is probably not safe for use with distributed sessions, ie
 * a "clustered" web application setup.
 * <p>
 * See {@link org.apache.myfaces.orchestra.conversation.ConversationWiperThread}
 * for more details.
 */
// TODO: rename this class to ConversationWiperThreadManager or similar; it is not just a
// SessionListener as it also implements ServletContextListener. This class specifically
// handles ConversationWiperThread issues...
public class ConversationManagerSessionListener
    implements
        ServletContextListener,
        HttpSessionListener, 
        HttpSessionAttributeListener,
        HttpSessionActivationListener
{
    private final Log log = LogFactory.getLog(ConversationManagerSessionListener.class);
    private final static long DEFAULT_CHECK_TIME = 5 * 60 * 1000; // every 5 min

    private final static String CHECK_TIME = "org.apache.myfaces.orchestra.WIPER_THREAD_CHECK_TIME"; // NON-NLS

    private ConversationWiperThread conversationWiperThread;

    public void contextInitialized(ServletContextEvent event)
    {
        log.debug("contextInitialized");
        long checkTime = DEFAULT_CHECK_TIME;
        String checkTimeString = event.getServletContext().getInitParameter(CHECK_TIME);
        if (checkTimeString != null)
        {
            checkTime = Long.parseLong(checkTimeString);
        }

        if (conversationWiperThread == null)
        {
            conversationWiperThread = new ConversationWiperThread(checkTime);
            conversationWiperThread.setName("Orchestra:ConversationWiperThread");
            conversationWiperThread.start();
        }
        else
        {
            log.error("context initialised more than once");
        }
        log.debug("initialised");
    }

    public void contextDestroyed(ServletContextEvent event)
    {
        log.debug("Context destroyed");
        if (conversationWiperThread != null)
        {
            conversationWiperThread.interrupt();
            conversationWiperThread = null;
        }
        else
        {
            log.error("Context destroyed more than once");
        }

    }

    public void sessionCreated(HttpSessionEvent event)
    {
        // Nothing to do here
    }

    public void sessionDestroyed(HttpSessionEvent event)
    {
        // If the session contains a ConversationManager, then remove it from the WiperThread.
        //
        // Note that for most containers, when a session is destroyed then attributeRemoved(x)
        // is called for each attribute in the session after this method is called. But some
        // containers (including OC4J) do not; it is therefore best to handle cleanup of the
        // ConversationWiperThread in both ways..
        //
        // Note that this method is called *before* the session is destroyed, ie the session is
        // still valid at this time.

        HttpSession session = event.getSession();
        Enumeration e = session.getAttributeNames();
        while (e.hasMoreElements())
        {
            String attrName = (String) e.nextElement();
            Object o = session.getAttribute(attrName);
            if (o instanceof ConversationManager)
            {
                // This call will trigger method "attributeRemoved" below, which will clean up the wiper thread.
                // And because the attribute is removed, the post-destroy calls to attributeRemoved will then
                // NOT include this (removed) attribute, so multiple attempts to clean it up will not occur.
                log.debug("Session containing a ConversationManager has been destroyed (eg timed out)");
                session.removeAttribute(attrName);
            }
        }
    }

    public void attributeAdded(HttpSessionBindingEvent event)
    {
        // Somebody has called session.setAttribute
        if (event.getValue() instanceof ConversationManager)
        {
            ConversationManager cm = (ConversationManager) event.getValue();
            conversationWiperThread.addConversationManager(cm);
        }
    }

    public void attributeRemoved(HttpSessionBindingEvent event)
    {
        // Either someone has called session.removeAttribute, or the session has been invalidated.
        // When an HttpSession is invalidated (including when it "times out"), first SessionDestroyed
        // is called, and then this method is called once for every attribute in the session; note
        // however that at that time the session is invalid so in some containers certain methods
        // (including getId and getAttribute) throw IllegalStateException.
        if (event.getValue() instanceof ConversationManager)
        {
            log.debug("A ConversationManager instance has been removed from a session");
            ConversationManager cm = (ConversationManager) event.getValue();
            removeAndInvalidateConversationManager(cm);
        }
    }

    public void attributeReplaced(HttpSessionBindingEvent event)
    {
        // Note that this method is called *after* the attribute has been replaced,
        // and that event.getValue contains the old object.
        if (event.getValue() instanceof ConversationManager)
        {
            ConversationManager oldConversationManager = (ConversationManager) event.getValue();
            removeAndInvalidateConversationManager(oldConversationManager);
        }

        // The new object is already in the session and can be retrieved from there
        HttpSession session = event.getSession();
        String attrName = event.getName();
        Object newObj = session.getAttribute(attrName);
        if (newObj instanceof ConversationManager)
        {
            ConversationManager newConversationManager = (ConversationManager) newObj;
            conversationWiperThread.addConversationManager(newConversationManager);
        }
    }

    /**
     * Run by the servlet container after deserializing an HttpSession.
     * <p>
     * This method tells the current ConversationWiperThread instance to start
     * monitoring all ConversationManager objects in the deserialized session.
     * 
     * @since 1.1
     */
    public void sessionDidActivate(HttpSessionEvent se)
    {
        // Reattach any ConversationManager objects in the session to the conversationWiperThread
        HttpSession session = se.getSession();
        Enumeration e = session.getAttributeNames();
        while (e.hasMoreElements())
        {
            String attrName = (String) e.nextElement();
            Object val = session.getAttribute(attrName);
            if (val instanceof ConversationManager)
            {
                // TODO: maybe touch the "last accessed" stamp for the conversation manager
                // and all its children? Without this, a conversation that has been passivated
                // might almost immediately get cleaned up after being reactivated.
                //
                // Hmm..actually, we should make sure the wiper thread never cleans up anything
                // associated with a session that is currently in use by a request. That should
                // then be sufficient, as the timeouts will only apply after the end of the
                // request that caused this activation to occur by which time any relevant
                // timestamps have been restored.
                ConversationManager cm = (ConversationManager) val;
                conversationWiperThread.addConversationManager(cm);
            }
        }
    }

    /**
     * Run by the servlet container before serializing an HttpSession.
     * <p>
     * This method tells the current ConversationWiperThread instance to stop
     * monitoring all ConversationManager objects in the serialized session.
     * 
     * @since 1.1
     */
    public void sessionWillPassivate(HttpSessionEvent se)
    {
        // Detach all ConversationManager objects in the session from the conversationWiperThread.
        // Without this, the ConversationManager and all its child objects would be kept in
        // memory as well as being passivated to external storage. Of course this does mean
        // that conversations in passivated sessions will not get timed out.
        HttpSession session = se.getSession();
        Enumeration e = session.getAttributeNames();
        while (e.hasMoreElements())
        {
            String attrName = (String) e.nextElement();
            Object val = session.getAttribute(attrName);
            if (val instanceof ConversationManager)
            {
                ConversationManager cm = (ConversationManager) val;
                conversationWiperThread.removeConversationManager(cm);
            }
        }
    }

    private void removeAndInvalidateConversationManager(ConversationManager cm)
    {
        // Note: When a session has timed out normally, then  currentFrameworkAdapter will
        // be null. But when a request calls session.invalidate directly, then this function
        // is called within the thread of the request, and so will have a FrameworkAdapter
        // in the current thread (which has been initialized with the http request object). 

        FrameworkAdapter currentFrameworkAdapter = FrameworkAdapter.getCurrentInstance();
        try
        {
            // Always use a fresh FrameworkAdapter to avoid OrchestraException
            // "Cannot remove current context" when a request calls session.invalidate();
            // we want getRequestParameter and related functions to always return null.. 
            FrameworkAdapter fa = new LocalFrameworkAdapter();
            ConversationMessager conversationMessager = new LogConversationMessager();
            fa.setConversationMessager(conversationMessager);
            FrameworkAdapter.setCurrentInstance(fa);
    
            conversationWiperThread.removeConversationManager(cm);
            cm.removeAndInvalidateAllConversationContexts();
        }
        finally
        {
            // Always restore original FrameworkAdapter.
            FrameworkAdapter.setCurrentInstance(currentFrameworkAdapter);

            if (currentFrameworkAdapter != null)
            {
                log.warn("removeAndInvalidateConversationManager: currentFrameworkAdapter is not null..");
            }
        }
    }
}
