/*
* 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)
{
if (log.isDebugEnabled())
{
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");
}
if (log.isDebugEnabled())
{
log.debug("initialised");
}
}
public void contextDestroyed(ServletContextEvent event)
{
if (log.isDebugEnabled())
{
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.
if (log.isDebugEnabled())
{
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)
{
if (log.isDebugEnabled())
{
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..");
}
}
}
}