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 }