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