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 }