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 }