View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one
    3    * or more contributor license agreements.  See the NOTICE file
    4    * distributed with this work for additional information
    5    * regarding copyright ownership.  The ASF licenses this file
    6    * to you under the Apache License, Version 2.0 (the
    7    * "License"); you may not use this file except in compliance
    8    * with the License.  You may obtain a copy of the License at
    9    *
    10   *   http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing,
    13   * software distributed under the License is distributed on an
    14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    15   * KIND, either express or implied.  See the License for the
    16   * specific language governing permissions and limitations
    17   * under the License.
    18   */
    19  package org.apache.myfaces.orchestra.conversation.servlet;
    20
    21  import java.util.Enumeration;
    22
    23  import org.apache.commons.logging.Log;
    24  import org.apache.commons.logging.LogFactory;
    25  import org.apache.myfaces.orchestra.conversation.ConversationManager;
    26  import org.apache.myfaces.orchestra.conversation.ConversationWiperThread;
    27  import org.apache.myfaces.orchestra.conversation.ConversationMessager;
    28  import org.apache.myfaces.orchestra.conversation.basic.LogConversationMessager;
    29  import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
    30  import org.apache.myfaces.orchestra.frameworkAdapter.local.LocalFrameworkAdapter;
    31
    32  import javax.servlet.ServletContextEvent;
    33  import javax.servlet.ServletContextListener;
    34  import javax.servlet.http.HttpSession;
    35  import javax.servlet.http.HttpSessionActivationListener;
    36  import javax.servlet.http.HttpSessionAttributeListener;
    37  import javax.servlet.http.HttpSessionBindingEvent;
    38  import javax.servlet.http.HttpSessionEvent;
    39  import javax.servlet.http.HttpSessionListener;
    40
    41  /**
    42   * An http session listener which periodically scans every http session for
    43   * conversations and conversation contexts that have exceeded their timeout.
    44   * <p>
    45   * If a web application wants to configure a conversation timeout that is
    46   * shorter than the http session timeout, then this class must be specified
    47   * as a listener in the web.xml file.
    48   * <p>
    49   * A conversation timeout is useful because the session timeout is refreshed
    50   * every time a request is made. If a user starts a conversation that uses
    51   * lots of memory, then abandons it and starts working elsewhere in the same
    52   * webapp then the session will continue to live, and therefore so will that
    53   * old "unused" conversation. Specifying a conversation timeout allows the
    54   * memory for that conversation to be reclaimed in this situation.
    55   * <p>
    56   * This listener starts a single background thread that periodically wakes
    57   * up and scans all http sessions to find ConversationContext objects, and
    58   * checks their timeout together with the timeout for all Conversations in
    59   * that context. If a conversation or context timeout has expired then it
    60   * is removed.
    61   * <p>
    62   * This code is probably not safe for use with distributed sessions, ie
    63   * a "clustered" web application setup.
    64   * <p>
    65   * See {@link org.apache.myfaces.orchestra.conversation.ConversationWiperThread}
    66   * for more details.
    67   */
    68  // TODO: rename this class to ConversationWiperThreadManager or similar; it is not just a
    69  // SessionListener as it also implements ServletContextListener. This class specifically
    70  // handles ConversationWiperThread issues...
    71  public class ConversationManagerSessionListener
    72      implements
    73          ServletContextListener,
    74          HttpSessionListener,
    75          HttpSessionAttributeListener,
    76          HttpSessionActivationListener
    77  {
    78      private final Log log = LogFactory.getLog(ConversationManagerSessionListener.class);
    79      private final static long DEFAULT_CHECK_TIME = 5 * 60 * 1000; // every 5 min
    80
    81      private final static String CHECK_TIME = "org.apache.myfaces.orchestra.WIPER_THREAD_CHECK_TIME"; // NON-NLS
    82
    83      private ConversationWiperThread conversationWiperThread;
    84
    85      public void contextInitialized(ServletContextEvent event)
    86      {
    87          if (log.isDebugEnabled())
    88          {
    89              log.debug("contextInitialized");
    90          }
    91          long checkTime = DEFAULT_CHECK_TIME;
    92          String checkTimeString = event.getServletContext().getInitParameter(CHECK_TIME);
    93          if (checkTimeString != null)
    94          {
    95              checkTime = Long.parseLong(checkTimeString);
    96          }
    97
    98          if (conversationWiperThread == null)
    99          {
    100             conversationWiperThread = new ConversationWiperThread(checkTime);
    101             conversationWiperThread.setName("Orchestra:ConversationWiperThread");
    102             conversationWiperThread.start();
    103         }
    104         else
    105         {
    106             log.error("context initialised more than once");
    107         }
    108         if (log.isDebugEnabled())
    109         {
    110             log.debug("initialised");
    111         }
    112     }
    113
    114     public void contextDestroyed(ServletContextEvent event)
    115     {
    116         if (log.isDebugEnabled())
    117         {
    118             log.debug("Context destroyed");
    119         }
    120         if (conversationWiperThread != null)
    121         {
    122             conversationWiperThread.interrupt();
    123             conversationWiperThread = null;
    124         }
    125         else
    126         {
    127             log.error("Context destroyed more than once");
    128         }
    129
    130     }
    131
    132     public void sessionCreated(HttpSessionEvent event)
    133     {
    134         // Nothing to do here
    135     }
    136
    137     public void sessionDestroyed(HttpSessionEvent event)
    138     {
    139         // If the session contains a ConversationManager, then remove it from the WiperThread.
    140         //
    141         // Note that for most containers, when a session is destroyed then attributeRemoved(x)
    142         // is called for each attribute in the session after this method is called. But some
    143         // containers (including OC4J) do not; it is therefore best to handle cleanup of the
    144         // ConversationWiperThread in both ways..
    145         //
    146         // Note that this method is called *before* the session is destroyed, ie the session is
    147         // still valid at this time.
    148
    149         HttpSession session = event.getSession();
    150         Enumeration e = session.getAttributeNames();
    151         while (e.hasMoreElements())
    152         {
    153             String attrName = (String) e.nextElement();
    154             Object o = session.getAttribute(attrName);
    155             if (o instanceof ConversationManager)
    156             {
    157                 // This call will trigger method "attributeRemoved" below, which will clean up the wiper thread.
    158                 // And because the attribute is removed, the post-destroy calls to attributeRemoved will then
    159                 // NOT include this (removed) attribute, so multiple attempts to clean it up will not occur.
    160                 if (log.isDebugEnabled())
    161                 {
    162                     log.debug("Session containing a ConversationManager has been destroyed (eg timed out)");
    163                 }
    164                 session.removeAttribute(attrName);
    165             }
    166         }
    167     }
    168
    169     public void attributeAdded(HttpSessionBindingEvent event)
    170     {
    171         // Somebody has called session.setAttribute
    172         if (event.getValue() instanceof ConversationManager)
    173         {
    174             ConversationManager cm = (ConversationManager) event.getValue();
    175             conversationWiperThread.addConversationManager(cm);
    176         }
    177     }
    178
    179     public void attributeRemoved(HttpSessionBindingEvent event)
    180     {
    181         // Either someone has called session.removeAttribute, or the session has been invalidated.
    182         // When an HttpSession is invalidated (including when it "times out"), first SessionDestroyed
    183         // is called, and then this method is called once for every attribute in the session; note
    184         // however that at that time the session is invalid so in some containers certain methods
    185         // (including getId and getAttribute) throw IllegalStateException.
    186         if (event.getValue() instanceof ConversationManager)
    187         {
    188             if (log.isDebugEnabled())
    189             {
    190                 log.debug("A ConversationManager instance has been removed from a session");
    191             }
    192             ConversationManager cm = (ConversationManager) event.getValue();
    193             removeAndInvalidateConversationManager(cm);
    194         }
    195     }
    196
    197     public void attributeReplaced(HttpSessionBindingEvent event)
    198     {
    199         // Note that this method is called *after* the attribute has been replaced,
    200         // and that event.getValue contains the old object.
    201         if (event.getValue() instanceof ConversationManager)
    202         {
    203             ConversationManager oldConversationManager = (ConversationManager) event.getValue();
    204             removeAndInvalidateConversationManager(oldConversationManager);
    205         }
    206
    207         // The new object is already in the session and can be retrieved from there
    208         HttpSession session = event.getSession();
    209         String attrName = event.getName();
    210         Object newObj = session.getAttribute(attrName);
    211         if (newObj instanceof ConversationManager)
    212         {
    213             ConversationManager newConversationManager = (ConversationManager) newObj;
    214             conversationWiperThread.addConversationManager(newConversationManager);
    215         }
    216     }
    217
    218     /**
    219      * Run by the servlet container after deserializing an HttpSession.
    220      * <p>
    221      * This method tells the current ConversationWiperThread instance to start
    222      * monitoring all ConversationManager objects in the deserialized session.
    223      *
    224      * @since 1.1
    225      */
    226     public void sessionDidActivate(HttpSessionEvent se)
    227     {
    228         // Reattach any ConversationManager objects in the session to the conversationWiperThread
    229         HttpSession session = se.getSession();
    230         Enumeration e = session.getAttributeNames();
    231         while (e.hasMoreElements())
    232         {
    233             String attrName = (String) e.nextElement();
    234             Object val = session.getAttribute(attrName);
    235             if (val instanceof ConversationManager)
    236             {
    237                 // TODO: maybe touch the "last accessed" stamp for the conversation manager
    238                 // and all its children? Without this, a conversation that has been passivated
    239                 // might almost immediately get cleaned up after being reactivated.
    240                 //
    241                 // Hmm..actually, we should make sure the wiper thread never cleans up anything
    242                 // associated with a session that is currently in use by a request. That should
    243                 // then be sufficient, as the timeouts will only apply after the end of the
    244                 // request that caused this activation to occur by which time any relevant
    245                 // timestamps have been restored.
    246                 ConversationManager cm = (ConversationManager) val;
    247                 conversationWiperThread.addConversationManager(cm);
    248             }
    249         }
    250     }
    251
    252     /**
    253      * Run by the servlet container before serializing an HttpSession.
    254      * <p>
    255      * This method tells the current ConversationWiperThread instance to stop
    256      * monitoring all ConversationManager objects in the serialized session.
    257      *
    258      * @since 1.1
    259      */
    260     public void sessionWillPassivate(HttpSessionEvent se)
    261     {
    262         // Detach all ConversationManager objects in the session from the conversationWiperThread.
    263         // Without this, the ConversationManager and all its child objects would be kept in
    264         // memory as well as being passivated to external storage. Of course this does mean
    265         // that conversations in passivated sessions will not get timed out.
    266         HttpSession session = se.getSession();
    267         Enumeration e = session.getAttributeNames();
    268         while (e.hasMoreElements())
    269         {
    270             String attrName = (String) e.nextElement();
    271             Object val = session.getAttribute(attrName);
    272             if (val instanceof ConversationManager)
    273             {
    274                 ConversationManager cm = (ConversationManager) val;
    275                 conversationWiperThread.removeConversationManager(cm);
    276             }
    277         }
    278     }
    279
    280     private void removeAndInvalidateConversationManager(ConversationManager cm)
    281     {
    282         // Note: When a session has timed out normally, then  currentFrameworkAdapter will
    283         // be null. But when a request calls session.invalidate directly, then this function
    284         // is called within the thread of the request, and so will have a FrameworkAdapter
    285         // in the current thread (which has been initialized with the http request object).
    286
    287         FrameworkAdapter currentFrameworkAdapter = FrameworkAdapter.getCurrentInstance();
    288         try
    289         {
    290             // Always use a fresh FrameworkAdapter to avoid OrchestraException
    291             // "Cannot remove current context" when a request calls session.invalidate();
    292             // we want getRequestParameter and related functions to always return null..
    293             FrameworkAdapter fa = new LocalFrameworkAdapter();
    294             ConversationMessager conversationMessager = new LogConversationMessager();
    295             fa.setConversationMessager(conversationMessager);
    296             FrameworkAdapter.setCurrentInstance(fa);
    297
    298             conversationWiperThread.removeConversationManager(cm);
    299             cm.removeAndInvalidateAllConversationContexts();
    300         }
    301         finally
    302         {
    303             // Always restore original FrameworkAdapter.
    304             FrameworkAdapter.setCurrentInstance(currentFrameworkAdapter);
    305
    306             if (currentFrameworkAdapter != null)
    307             {
    308                 log.warn("removeAndInvalidateConversationManager: currentFrameworkAdapter is not null..");
    309             }
    310         }
    311     }
    312 }