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  
28  import javax.servlet.ServletContextEvent;
29  import javax.servlet.ServletContextListener;
30  import javax.servlet.http.HttpSession;
31  import javax.servlet.http.HttpSessionActivationListener;
32  import javax.servlet.http.HttpSessionAttributeListener;
33  import javax.servlet.http.HttpSessionBindingEvent;
34  import javax.servlet.http.HttpSessionEvent;
35  import javax.servlet.http.HttpSessionListener;
36  
37  /**
38   * An http session listener which periodically scans every http session for
39   * conversations and conversation contexts that have exceeded their timeout.
40   * <p>
41   * If a web application wants to configure a conversation timeout that is
42   * shorter than the http session timeout, then this class must be specified
43   * as a listener in the web.xml file.
44   * <p>
45   * A conversation timeout is useful because the session timeout is refreshed
46   * every time a request is made. If a user starts a conversation that uses
47   * lots of memory, then abandons it and starts working elsewhere in the same
48   * webapp then the session will continue to live, and therefore so will that
49   * old "unused" conversation. Specifying a conversation timeout allows the
50   * memory for that conversation to be reclaimed in this situation.
51   * <p>
52   * This listener starts a single background thread that periodically wakes
53   * up and scans all http sessions to find ConversationContext objects, and
54   * checks their timeout together with the timeout for all Conversations in
55   * that context. If a conversation or context timeout has expired then it
56   * is removed.
57   * <p>
58   * This code is probably not safe for use with distributed sessions, ie
59   * a "clustered" web application setup.
60   * <p>
61   * See {@link org.apache.myfaces.orchestra.conversation.ConversationWiperThread}
62   * for more details.
63   */
64  // TODO: rename this class to ConversationWiperThreadManager or similar; it is not just a
65  // SessionListener as it also implements ServletContextListener. This class specifically
66  // handles ConversationWiperThread issues...
67  public class ConversationManagerSessionListener
68      implements
69          ServletContextListener,
70          HttpSessionListener, 
71          HttpSessionAttributeListener,
72          HttpSessionActivationListener
73  {
74      private final Log log = LogFactory.getLog(ConversationManagerSessionListener.class);
75      private final static long DEFAULT_CHECK_TIME = 5 * 60 * 1000; // every 5 min
76  
77      private final static String CHECK_TIME = "org.apache.myfaces.orchestra.WIPER_THREAD_CHECK_TIME"; // NON-NLS
78  
79      private ConversationWiperThread conversationWiperThread;
80  
81      public void contextInitialized(ServletContextEvent event)
82      {
83          log.debug("contextInitialized");
84          long checkTime = DEFAULT_CHECK_TIME;
85          String checkTimeString = event.getServletContext().getInitParameter(CHECK_TIME);
86          if (checkTimeString != null)
87          {
88              checkTime = Long.parseLong(checkTimeString);
89          }
90  
91          if (conversationWiperThread == null)
92          {
93              conversationWiperThread = new ConversationWiperThread(checkTime);
94              conversationWiperThread.setName("Orchestra:ConversationWiperThread");
95              conversationWiperThread.start();
96          }
97          else
98          {
99              log.error("context initialised more than once");
100         }
101         log.debug("initialised");
102     }
103 
104     public void contextDestroyed(ServletContextEvent event)
105     {
106         log.debug("Context destroyed");
107         if (conversationWiperThread != null)
108         {
109             conversationWiperThread.interrupt();
110             conversationWiperThread = null;
111         }
112         else
113         {
114             log.error("Context destroyed more than once");
115         }
116 
117     }
118 
119     public void sessionCreated(HttpSessionEvent event)
120     {
121         // Nothing to do here
122     }
123 
124     public void sessionDestroyed(HttpSessionEvent event)
125     {
126         // If the session contains a ConversationManager, then remove it from the WiperThread.
127         //
128         // Note that for most containers, when a session is destroyed then attributeRemoved(x)
129         // is called for each attribute in the session after this method is called. But some
130         // containers (including OC4J) do not; it is therefore best to handle cleanup of the
131         // ConversationWiperThread in both ways..
132         //
133         // Note that this method is called *before* the session is destroyed, ie the session is
134         // still valid at this time.
135 
136         HttpSession session = event.getSession();
137         Enumeration e = session.getAttributeNames();
138         while (e.hasMoreElements())
139         {
140             String attrName = (String) e.nextElement();
141             Object o = session.getAttribute(attrName);
142             if (o instanceof ConversationManager)
143             {
144                 // This call will trigger method "attributeRemoved" below, which will clean up the wiper thread.
145                 // And because the attribute is removed, the post-destroy calls to attributeRemoved will then
146                 // NOT include this (removed) attribute, so multiple attempts to clean it up will not occur.
147                 log.debug("Session containing a ConversationManager has been destroyed (eg timed out)");
148                 session.removeAttribute(attrName);
149             }
150         }
151     }
152 
153     public void attributeAdded(HttpSessionBindingEvent event)
154     {
155         // Somebody has called session.setAttribute
156         if (event.getValue() instanceof ConversationManager)
157         {
158             ConversationManager cm = (ConversationManager) event.getValue();
159             conversationWiperThread.addConversationManager(cm);
160         }
161     }
162 
163     public void attributeRemoved(HttpSessionBindingEvent event)
164     {
165         // Either someone has called session.removeAttribute, or the session has been invalidated.
166         // When an HttpSession is invalidated (including when it "times out"), first SessionDestroyed
167         // is called, and then this method is called once for every attribute in the session; note
168         // however that at that time the session is invalid so in some containers certain methods
169         // (including getId and getAttribute) throw IllegalStateException.
170         if (event.getValue() instanceof ConversationManager)
171         {
172             log.debug("A ConversationManager instance has been removed from a session");
173             ConversationManager cm = (ConversationManager) event.getValue();
174             conversationWiperThread.removeConversationManager(cm);
175         }
176     }
177 
178     public void attributeReplaced(HttpSessionBindingEvent event)
179     {
180         // Note that this method is called *after* the attribute has been replaced,
181         // and that event.getValue contains the old object.
182         if (event.getValue() instanceof ConversationManager)
183         {
184             ConversationManager oldConversationManager = (ConversationManager) event.getValue();
185             conversationWiperThread.removeConversationManager(oldConversationManager);
186         }
187 
188         // The new object is already in the session and can be retrieved from there
189         HttpSession session = event.getSession();
190         String attrName = event.getName();
191         Object newObj = session.getAttribute(attrName);
192         if (newObj instanceof ConversationManager)
193         {
194             ConversationManager newConversationManager = (ConversationManager) newObj;
195             conversationWiperThread.addConversationManager(newConversationManager);
196         }
197     }
198 
199     /**
200      * Run by the servlet container after deserializing an HttpSession.
201      * <p>
202      * This method tells the current ConversationWiperThread instance to start
203      * monitoring all ConversationManager objects in the deserialized session.
204      * 
205      * @since 1.1
206      */
207     public void sessionDidActivate(HttpSessionEvent se)
208     {
209         // Reattach any ConversationManager objects in the session to the conversationWiperThread
210         HttpSession session = se.getSession();
211         Enumeration e = session.getAttributeNames();
212         while (e.hasMoreElements())
213         {
214             String attrName = (String) e.nextElement();
215             Object val = session.getAttribute(attrName);
216             if (val instanceof ConversationManager)
217             {
218                 // TODO: maybe touch the "last accessed" stamp for the conversation manager
219                 // and all its children? Without this, a conversation that has been passivated
220                 // might almost immediately get cleaned up after being reactivated.
221                 //
222                 // Hmm..actually, we should make sure the wiper thread never cleans up anything
223                 // associated with a session that is currently in use by a request. That should
224                 // then be sufficient, as the timeouts will only apply after the end of the
225                 // request that caused this activation to occur by which time any relevant
226                 // timestamps have been restored.
227                 ConversationManager cm = (ConversationManager) val;
228                 conversationWiperThread.addConversationManager(cm);
229             }
230         }
231     }
232 
233     /**
234      * Run by the servlet container before serializing an HttpSession.
235      * <p>
236      * This method tells the current ConversationWiperThread instance to stop
237      * monitoring all ConversationManager objects in the serialized session.
238      * 
239      * @since 1.1
240      */
241     public void sessionWillPassivate(HttpSessionEvent se)
242     {
243         // Detach all ConversationManager objects in the session from the conversationWiperThread.
244         // Without this, the ConversationManager and all its child objects would be kept in
245         // memory as well as being passivated to external storage. Of course this does mean
246         // that conversations in passivated sessions will not get timed out.
247         HttpSession session = se.getSession();
248         Enumeration e = session.getAttributeNames();
249         while (e.hasMoreElements())
250         {
251             String attrName = (String) e.nextElement();
252             Object val = session.getAttribute(attrName);
253             if (val instanceof ConversationManager)
254             {
255                 ConversationManager cm = (ConversationManager) val;
256                 conversationWiperThread.removeConversationManager(cm);
257             }
258         }
259     }
260 }