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          log.debug("contextInitialized");
    88          long checkTime = DEFAULT_CHECK_TIME;
    89          String checkTimeString = event.getServletContext().getInitParameter(CHECK_TIME);
    90          if (checkTimeString != null)
    91          {
    92              checkTime = Long.parseLong(checkTimeString);
    93          }
    94
    95          if (conversationWiperThread == null)
    96          {
    97              conversationWiperThread = new ConversationWiperThread(checkTime);
    98              conversationWiperThread.setName("Orchestra:ConversationWiperThread");
    99              conversationWiperThread.start();
    100         }
    101         else
    102         {
    103             log.error("context initialised more than once");
    104         }
    105         log.debug("initialised");
    106     }
    107
    108     public void contextDestroyed(ServletContextEvent event)
    109     {
    110         log.debug("Context destroyed");
    111         if (conversationWiperThread != null)
    112         {
    113             conversationWiperThread.interrupt();
    114             conversationWiperThread = null;
    115         }
    116         else
    117         {
    118             log.error("Context destroyed more than once");
    119         }
    120
    121     }
    122
    123     public void sessionCreated(HttpSessionEvent event)
    124     {
    125         // Nothing to do here
    126     }
    127
    128     public void sessionDestroyed(HttpSessionEvent event)
    129     {
    130         // If the session contains a ConversationManager, then remove it from the WiperThread.
    131         //
    132         // Note that for most containers, when a session is destroyed then attributeRemoved(x)
    133         // is called for each attribute in the session after this method is called. But some
    134         // containers (including OC4J) do not; it is therefore best to handle cleanup of the
    135         // ConversationWiperThread in both ways..
    136         //
    137         // Note that this method is called *before* the session is destroyed, ie the session is
    138         // still valid at this time.
    139
    140         HttpSession session = event.getSession();
    141         Enumeration e = session.getAttributeNames();
    142         while (e.hasMoreElements())
    143         {
    144             String attrName = (String) e.nextElement();
    145             Object o = session.getAttribute(attrName);
    146             if (o instanceof ConversationManager)
    147             {
    148                 // This call will trigger method "attributeRemoved" below, which will clean up the wiper thread.
    149                 // And because the attribute is removed, the post-destroy calls to attributeRemoved will then
    150                 // NOT include this (removed) attribute, so multiple attempts to clean it up will not occur.
    151                 log.debug("Session containing a ConversationManager has been destroyed (eg timed out)");
    152                 session.removeAttribute(attrName);
    153             }
    154         }
    155     }
    156
    157     public void attributeAdded(HttpSessionBindingEvent event)
    158     {
    159         // Somebody has called session.setAttribute
    160         if (event.getValue() instanceof ConversationManager)
    161         {
    162             ConversationManager cm = (ConversationManager) event.getValue();
    163             conversationWiperThread.addConversationManager(cm);
    164         }
    165     }
    166
    167     public void attributeRemoved(HttpSessionBindingEvent event)
    168     {
    169         // Either someone has called session.removeAttribute, or the session has been invalidated.
    170         // When an HttpSession is invalidated (including when it "times out"), first SessionDestroyed
    171         // is called, and then this method is called once for every attribute in the session; note
    172         // however that at that time the session is invalid so in some containers certain methods
    173         // (including getId and getAttribute) throw IllegalStateException.
    174         if (event.getValue() instanceof ConversationManager)
    175         {
    176             log.debug("A ConversationManager instance has been removed from a session");
    177             ConversationManager cm = (ConversationManager) event.getValue();
    178             removeAndInvalidateConversationManager(cm);
    179         }
    180     }
    181
    182     public void attributeReplaced(HttpSessionBindingEvent event)
    183     {
    184         // Note that this method is called *after* the attribute has been replaced,
    185         // and that event.getValue contains the old object.
    186         if (event.getValue() instanceof ConversationManager)
    187         {
    188             ConversationManager oldConversationManager = (ConversationManager) event.getValue();
    189             removeAndInvalidateConversationManager(oldConversationManager);
    190         }
    191
    192         // The new object is already in the session and can be retrieved from there
    193         HttpSession session = event.getSession();
    194         String attrName = event.getName();
    195         Object newObj = session.getAttribute(attrName);
    196         if (newObj instanceof ConversationManager)
    197         {
    198             ConversationManager newConversationManager = (ConversationManager) newObj;
    199             conversationWiperThread.addConversationManager(newConversationManager);
    200         }
    201     }
    202
    203     /**
    204      * Run by the servlet container after deserializing an HttpSession.
    205      * <p>
    206      * This method tells the current ConversationWiperThread instance to start
    207      * monitoring all ConversationManager objects in the deserialized session.
    208      *
    209      * @since 1.1
    210      */
    211     public void sessionDidActivate(HttpSessionEvent se)
    212     {
    213         // Reattach any ConversationManager objects in the session to the conversationWiperThread
    214         HttpSession session = se.getSession();
    215         Enumeration e = session.getAttributeNames();
    216         while (e.hasMoreElements())
    217         {
    218             String attrName = (String) e.nextElement();
    219             Object val = session.getAttribute(attrName);
    220             if (val instanceof ConversationManager)
    221             {
    222                 // TODO: maybe touch the "last accessed" stamp for the conversation manager
    223                 // and all its children? Without this, a conversation that has been passivated
    224                 // might almost immediately get cleaned up after being reactivated.
    225                 //
    226                 // Hmm..actually, we should make sure the wiper thread never cleans up anything
    227                 // associated with a session that is currently in use by a request. That should
    228                 // then be sufficient, as the timeouts will only apply after the end of the
    229                 // request that caused this activation to occur by which time any relevant
    230                 // timestamps have been restored.
    231                 ConversationManager cm = (ConversationManager) val;
    232                 conversationWiperThread.addConversationManager(cm);
    233             }
    234         }
    235     }
    236
    237     /**
    238      * Run by the servlet container before serializing an HttpSession.
    239      * <p>
    240      * This method tells the current ConversationWiperThread instance to stop
    241      * monitoring all ConversationManager objects in the serialized session.
    242      *
    243      * @since 1.1
    244      */
    245     public void sessionWillPassivate(HttpSessionEvent se)
    246     {
    247         // Detach all ConversationManager objects in the session from the conversationWiperThread.
    248         // Without this, the ConversationManager and all its child objects would be kept in
    249         // memory as well as being passivated to external storage. Of course this does mean
    250         // that conversations in passivated sessions will not get timed out.
    251         HttpSession session = se.getSession();
    252         Enumeration e = session.getAttributeNames();
    253         while (e.hasMoreElements())
    254         {
    255             String attrName = (String) e.nextElement();
    256             Object val = session.getAttribute(attrName);
    257             if (val instanceof ConversationManager)
    258             {
    259                 ConversationManager cm = (ConversationManager) val;
    260                 conversationWiperThread.removeConversationManager(cm);
    261             }
    262         }
    263     }
    264
    265     private void removeAndInvalidateConversationManager(ConversationManager cm)
    266     {
    267         // Note: When a session has timed out normally, then  currentFrameworkAdapter will
    268         // be null. But when a request calls session.invalidate directly, then this function
    269         // is called within the thread of the request, and so will have a FrameworkAdapter
    270         // in the current thread (which has been initialized with the http request object).
    271
    272         FrameworkAdapter currentFrameworkAdapter = FrameworkAdapter.getCurrentInstance();
    273         try
    274         {
    275             // Always use a fresh FrameworkAdapter to avoid OrchestraException
    276             // "Cannot remove current context" when a request calls session.invalidate();
    277             // we want getRequestParameter and related functions to always return null..
    278             FrameworkAdapter fa = new LocalFrameworkAdapter();
    279             ConversationMessager conversationMessager = new LogConversationMessager();
    280             fa.setConversationMessager(conversationMessager);
    281             FrameworkAdapter.setCurrentInstance(fa);
    282
    283             conversationWiperThread.removeConversationManager(cm);
    284             cm.removeAndInvalidateAllConversationContexts();
    285         }
    286         finally
    287         {
    288             // Always restore original FrameworkAdapter.
    289             FrameworkAdapter.setCurrentInstance(currentFrameworkAdapter);
    290
    291             if (currentFrameworkAdapter != null)
    292             {
    293                 log.warn("removeAndInvalidateConversationManager: currentFrameworkAdapter is not null..");
    294             }
    295         }
    296     }
    297 }