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  
20  package org.apache.myfaces.orchestra.conversation;
21  
22  import java.io.IOException;
23  import java.io.ObjectStreamException;
24  import java.io.Serializable;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.Iterator;
28  import java.util.Map;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
33  import org.apache.myfaces.orchestra.requestParameterProvider.RequestParameterProviderManager;
34  
35  /***
36   * Deals with the various conversation contexts in the current session.
37   * <p>
38   * A new conversation context will be created if the servlet request did
39   * not specify an existing conversation context id.
40   * <p>
41   * At the current time, this object does not serialize well. Any attempt to serialize
42   * this object (including any serialization of the user session) will just cause it
43   * to be discarded.
44   * <p>
45   * TODO: fix serialization issues.
46   */
47  public class ConversationManager implements Serializable
48  {
49  	final static String CONVERSATION_CONTEXT_PARAM = "conversationContext";
50  
51  	private final static String CONVERSATION_MANAGER_KEY = "org.apache.myfaces.ConversationManager";
52  	private final static String CONVERSATION_CONTEXT_REQ = "org.apache.myfaces.ConversationManager.conversationContext";
53  
54  	private static final Iterator EMPTY_ITERATOR = Collections.EMPTY_LIST.iterator();
55  
56  	// TODO: consider whether a static value is the best way to generate context ids; they
57  	// only need to be unique within a user session. Statics are also test-unfriendly.
58  	private static long nextConversationContextId = 1;
59  
60  	private final Log log = LogFactory.getLog(ConversationManager.class);
61  
62  	// This member must always be accessed with a lock held on the parent ConverstationManager instance;
63  	// a HashMap is not thread-safe and this class must be thread-safe.
64  	private final Map conversationContexts = new HashMap();
65  
66  	protected ConversationManager()
67  	{
68  		// conversationMessager = createMessager();
69  	}
70  
71  	/***
72  	 * Get the conversation manager. This creates a new one if none exists.
73  	 */
74  	public static ConversationManager getInstance()
75  	{
76  		return getInstance(true);
77  	}
78  
79  	/***
80  	 * Get the conversation manager.
81  	 * <p>
82  	 * When create is true, an instance is always returned; one is
83  	 * created if none currently exists for the current user session.
84  	 * <p>
85  	 * When create is false, null is returned if no instance yet
86  	 * exists for the current user session.
87  	 */
88  	public static ConversationManager getInstance(boolean create)
89  	{
90  		ConversationManager conversationManager = (ConversationManager) FrameworkAdapter.getCurrentInstance().getSessionAttribute(CONVERSATION_MANAGER_KEY);
91  		if (conversationManager == null && create)
92  		{
93  			// TODO: do not call new directly here, as it makes it impossible to configure
94  			// an alternative ConversationManager instance. This is IOC and test unfriendly.
95  			conversationManager = new ConversationManager();
96  
97  			// initialize environmental systems
98  			RequestParameterProviderManager.getInstance().register(new ConversationRequestParameterProvider());
99  
100 			// set mark
101 			FrameworkAdapter.getCurrentInstance().setSessionAttribute(CONVERSATION_MANAGER_KEY, conversationManager);
102 		}
103 
104 		return conversationManager;
105 	}
106 
107 	/***
108 	 * Get the current, or create a new unique conversationContextId.
109 	 * <p>
110 	 * The current conversationContextId will retrieved from the request parameters. If no such parameter is
111 	 * present then a new id will be allocated and a new ConversationContext created.
112 	 * <p>
113 	 * In either case the result will be stored within the request for faster lookup.
114 	 * <p>
115 	 * Note that there is no security flaw regarding injection of fake context ids; the id must match one already
116 	 * in the session and there is no security problem with two windows in the same session exchanging ids.
117 	 */
118 	public Long getConversationContextId()
119 	{
120 		Long conversationContextId = (Long) FrameworkAdapter.getCurrentInstance().getRequestAttribute(CONVERSATION_CONTEXT_REQ);
121 		if (conversationContextId == null)
122 		{
123 			if (FrameworkAdapter.getCurrentInstance().containsRequestParameterAttribute(CONVERSATION_CONTEXT_PARAM))
124 			{
125 				String urlConversationContextId = FrameworkAdapter.getCurrentInstance().getRequestParameterAttribute(CONVERSATION_CONTEXT_PARAM).toString();
126 				conversationContextId = new Long(Long.parseLong(urlConversationContextId, Character.MAX_RADIX));
127 			}
128 			else
129 			{
130 				synchronized (ConversationManager.class)
131 				{
132 					conversationContextId = new Long(nextConversationContextId);
133 					nextConversationContextId++;
134 				}
135 			}
136 
137 			FrameworkAdapter.getCurrentInstance().setRequestAttribute(CONVERSATION_CONTEXT_REQ, conversationContextId);
138 		}
139 
140 		return conversationContextId;
141 	}
142 
143 	/***
144 	 * Get the conversation context for the given id
145 	 */
146 	protected ConversationContext getConversationContext(Long conversationContextId)
147 	{
148 		synchronized (this)
149 		{
150 			return (ConversationContext) conversationContexts.get(conversationContextId);
151 		}
152 	}
153 
154 	/***
155 	 * Get the conversation context for the given id. <br />
156 	 * If there is no conversation context a new one will be created
157 	 */
158 	protected ConversationContext getOrCreateConversationContext(Long conversationContextId)
159 	{
160 		synchronized (this)
161 		{
162 			ConversationContext conversationContext = (ConversationContext) conversationContexts.get(conversationContextId);
163 			if (conversationContext == null)
164 			{
165 				conversationContext = new ConversationContext(conversationContextId.longValue());
166 				conversationContexts.put(conversationContextId, conversationContext);
167 			}
168 
169 			return conversationContext;
170 		}
171 	}
172 
173 	/***
174 	 * Ends all conversations within the current context; the context itself will remain active.
175 	 */
176 	public void clearCurrentConversationContext()
177 	{
178 		Long conversationContextId = getConversationContextId();
179 		if (conversationContextId == null)
180 		{
181 			return;
182 		}
183 
184 		ConversationContext conversationContext = getConversationContext(conversationContextId);
185 		if (conversationContext != null)
186 		{
187 			conversationContext.clear();
188 		}
189 	}
190 
191 	/***
192 	 * <p>Destroy the given conversation context</p>
193 	 * <p/>
194 	 * <p>Notice: its assumed that the context is already been destroyed</p>
195 	 */
196 	protected void removeConversationContext(Long conversationContextId)
197 	{
198 		synchronized (this)
199 		{
200 			conversationContexts.remove(conversationContextId);
201 		}
202 	}
203 
204 	/***
205 	 * Start a conversation.
206 	 *
207 	 * @see ConversationContext#startConversation(String, ConversationFactory)
208 	 */
209 	public Conversation startConversation(String name, ConversationFactory factory)
210 	{
211 		Long conversationContextId = getConversationContextId();
212 		ConversationContext conversationContext = getOrCreateConversationContext(conversationContextId);
213 		return conversationContext.startConversation(name, factory);
214 	}
215 
216 	/***
217 	 * Remove a conversation
218 	 *
219 	 * <p>Notice: Its assumed that the conversation has already been invalidated</p>
220 	 *
221 	 * @see ConversationContext#removeConversation(String)
222 	 */
223 	protected void removeConversation(String name)
224 	{
225 		Long conversationContextId = getConversationContextId();
226 		ConversationContext conversationContext = getConversationContext(conversationContextId);
227 		if (conversationContext != null)
228 		{
229 			conversationContext.removeConversation(name);
230 		}
231 	}
232 
233 	/***
234 	 * Get the conversation with the given name
235 	 *
236 	 * @return null if no conversation context is active or if the conversation did not exist.
237 	 */
238 	public Conversation getConversation(String name)
239 	{
240 		ConversationContext conversationContext = getCurrentConversationContext();
241 		if (conversationContext == null)
242 		{
243 			return null;
244 		}
245 		return conversationContext.getConversation(name);
246 	}
247 
248 	/***
249 	 * check if the given conversation is active
250 	 */
251 	public boolean hasConversation(String name)
252 	{
253 		ConversationContext conversationContext = getCurrentConversationContext();
254 		if (conversationContext == null)
255 		{
256 			return false;
257 		}
258 		return conversationContext.hasConversation(name);
259 	}
260 
261 	/***
262 	 * Returns an iterator over all the Conversation objects in the current conversation
263 	 * context. Never returns null, even if no conversation context exists.
264 	 */
265 	public Iterator iterateConversations()
266 	{
267 		ConversationContext conversationContext = getCurrentConversationContext();
268 		if (conversationContext == null)
269 		{
270 			return EMPTY_ITERATOR;
271 		}
272 
273 		return conversationContext.iterateConversations();
274 	}
275 
276 	/***
277 	 * Get the current conversation context.
278 	 *
279 	 * @return null if there is no context active
280 	 */
281 	public ConversationContext getCurrentConversationContext()
282 	{
283 		Long conversationContextId = getConversationContextId();
284 		if (conversationContextId == null)
285 		{
286 			return null;
287 		}
288 
289 		ConversationContext conversationContext = getConversationContext(conversationContextId);
290 		return conversationContext;
291 	}
292 
293 	/***
294 	 * check if we have a conversation context
295 	 */
296 	public boolean hasConversationContext()
297 	{
298 		return
299 			(
300 				FrameworkAdapter.getCurrentInstance().containsRequestAttribute(CONVERSATION_CONTEXT_REQ) ||
301 					FrameworkAdapter.getCurrentInstance().containsRequestParameterAttribute(CONVERSATION_CONTEXT_REQ)) &&
302 				getCurrentConversationContext() != null;
303 	}
304 
305 	/***
306 	 * Get the Messager used to inform the user about anomalies.
307 	 * <p>
308 	 * What instance is returned is controlled by the FrameworkAdapter. See 
309 	 * {@link org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter} for details.
310 	 */
311 	public ConversationMessager getMessager()
312 	{
313 		return FrameworkAdapter.getCurrentInstance().getConversationMessager();
314 	}
315 
316 	/***
317 	 * Check the timeout for each conversation context, and all conversations
318 	 * within those contexts.
319 	 * <p>
320 	 * If any conversation has not been accessed within its timeout period
321 	 * then clear the context.
322 	 * <p>
323 	 * Invoke the checkTimeout method on each context so that any conversation
324 	 * that has not been accessed within its timeout is invalidated.
325 	 */
326 	protected void checkTimeouts()
327 	{
328 		Map.Entry[] contexts;
329 		synchronized (this)
330 		{
331 			contexts = new Map.Entry[conversationContexts.size()];
332 			conversationContexts.entrySet().toArray(contexts);
333 		}
334 
335 		long checkTime = System.currentTimeMillis();
336 
337 		for (int i = 0; i<contexts.length; i++)
338 		{
339 			Map.Entry context = contexts[i];
340 
341 			Long conversationContextId = (Long) context.getKey();
342 			ConversationContext conversationContext = (ConversationContext) context.getValue();
343 			conversationContext.checkConversationTimeout();
344 
345 			if (conversationContext.getTimeout() > -1 &&
346 				(conversationContext.getLastAccess() +
347 				conversationContext.getTimeout()) < checkTime)
348 			{
349 				if (log.isDebugEnabled())
350 				{
351 					log.debug("end conversation context due to timeout: " + conversationContext.getId());
352 				}
353 
354 				conversationContext.clear();
355 				synchronized (this)
356 				{
357 					conversationContexts.remove(conversationContextId);
358 				}
359 			}
360 		}
361 	}
362 
363 	/*
364 	 * check if a bean with the given name will be managed by the conversationManager.
365 	public boolean isManagedBean(String beanName)
366 	{
367 		ConfigurableApplicationContext applicationContext = FrameworkAdapter.getInstance().getApplicationContext();
368 		ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
369 
370 		// XXX: Hack to get the real definition in case of scoped targets
371 		BeanDefinition beanDefinition = beanFactory.getBeanDefinition("scopedTarget." + beanName);
372 		if (beanDefinition == null)
373 		{
374 			beanDefinition = beanFactory.getBeanDefinition(beanName);
375 		}
376 
377 		return beanDefinition.getScope() != null && managedScopes.contains(beanDefinition.getScope());
378 	}
379 	 */
380 
381 	private void writeObject(java.io.ObjectOutputStream out) throws IOException
382 	{
383 		// the conversation manager is not (yet) serializable, we just implement it
384 		// to make it work with distributed sessions
385 	}
386 
387 	private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
388 	{
389 		// nothing written, so nothing to read
390 	}
391 
392 	private Object readResolve() throws ObjectStreamException
393 	{
394 		// do not return a real object, that way on first request a new conversation manager will be created
395 		return null;
396 	}
397 }