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 }