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.util.Map;
23  import java.util.TreeMap;
24  
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  
28  /***
29   * A Conversation is a container for a set of beans.
30   * 
31   * <p>Optionally, a PersistenceContext can also be associated with a conversation.</p>
32   * 
33   * <p>There are various ways how to get access to a Conversation instance:
34   * <ul>
35   * <li>{@link Conversation#getCurrentInstance} if you are calling from a
36   * conversation-scoped bean, or something that is called from such a bean.</li>
37   * <li>{@link ConversationManager#getConversation(String)}</li>
38   * <li>by implementing the {@link ConversationAware} or {@link ConversationBindingListener}
39   * interface in a bean.</li>
40   * </ul>
41   * </p>
42   * 
43   * <p>Conversation instances are typically created when an EL expression references a
44   * bean whose definition indicates that it is in a conversation scope.</p>
45   * 
46   * <p>Conversation instances are typically destroyed:
47   * <ul>
48   * <li>At the end of a request when they are marked as a "flash" conversation but
49   * no bean in the conversation scope was accessed during the just-completed request.</li>
50   * <li>Via an ox:endConversation component</li>
51   * <li>Via an action method calling Conversation.invalidate()</li>
52   * <li>Due to a conversation timeout, ie when no object in the conversation has been
53   * accessed for N minutes. See ConversationManagedSessionListener,
54   * ConversationTimeoutableAspect, and ConversationManager.checkTimeouts.</li>
55   * </ul>
56   * </p>
57   */
58  public class Conversation
59  {
60  	// See getCurrentInstance, setCurrentInstance and class CurrentConversationAdvice.
61  	private final static ThreadLocal CURRENT_CONVERSATION = new ThreadLocal();
62  
63  	private final Log log = LogFactory.getLog(Conversation.class);
64  
65  	private final String name;
66  	
67  	// The factory that created this conversation instance; needed
68  	// when restarting the conversation.
69  	private final ConversationFactory factory;
70  
71  	// The set of managed beans that are associated with this conversation.
72  	private final Map beans = new TreeMap();
73  
74  	// The parent context to which this conversation belongs. This is needed
75  	// when restarting the conversation.
76  	private final ConversationContext conversationContext;
77  
78  	// See addAspect.
79  	private ConversationAspects conversationAspects = new ConversationAspects();
80  
81  	// Is this object usable, or "destroyed"?
82  	private boolean invalid = false;
83  
84  	// Is this object going to be destroyed as soon as it is no longer "active"?
85  	private boolean queueInvalid = false;
86  
87  	// system timestamp in milliseconds at which this object was last accessed;
88  	// see method touch().
89  	private long lastAccess;
90  
91  	private Object activeCountMutex = new Object();
92  	private int activeCount;
93  
94  	public Conversation(ConversationContext conversationContext, String name, ConversationFactory factory)
95  	{
96  		this.conversationContext = conversationContext;
97  		this.name = name;
98  		this.factory = factory;
99  
100 		if (log.isDebugEnabled())
101 		{
102 			log.debug("start conversation:" + name);
103 		}
104 
105 		touch();
106 	}
107 
108 	/***
109 	 * Mark this conversation as having been used at the current time.
110 	 * <p>
111 	 * Conversations can have "timeouts" associated with them, so that when a user stops
112 	 * a conversation and goes off to work on some other part of the webapp then the
113 	 * conversation's memory can eventually be reclaimed.
114 	 * <p>
115 	 * Whenever user code causes this conversation object to be looked up and returned,
116 	 * this "touch" method is invoked to indicate that the conversation is in use. Direct
117 	 * conversation lookups by user code can occur, but the most common access is expected
118 	 * to be via an EL expression which a lookup of a bean that is declared as being in
119 	 * conversation scope. The bean lookup causes the corresponding conversation to be
120 	 * looked up, which triggers this method.
121 	 */
122 	protected void touch()
123 	{
124 		lastAccess = System.currentTimeMillis();
125 	}
126 
127 	/***
128 	 * The system time in millis when this conversation has been accessed last
129 	 */
130 	public long getLastAccess()
131 	{
132 		return lastAccess;
133 	}
134 
135 	/***
136 	 * Add the given bean to the conversation scope.
137 	 * 
138 	 * <p>This will fire a {@link ConversationBindingEvent} on the bean parameter
139 	 * object if the bean implements the {@link ConversationBindingListener}
140 	 * interface</p>
141 	 * 
142 	 * <p>Note that any object can be stored into the conversation; it is not
143 	 * limited to managed beans declared in a configuration file. This
144 	 * feature is not expected to be heavily used however; most attributes of
145 	 * a conversation are expected to be externally-declared "managed beans".</p>
146 	 */
147 	public void setAttribute(String name, Object bean)
148 	{
149 		checkValid();
150 
151 		synchronized(conversationContext)
152 		{
153 			removeAttribute(name);
154 
155 			if (log.isDebugEnabled())
156 			{
157 				log.debug("put bean to conversation:" + name + "(bean=" + bean + ")");
158 			}
159 
160 			beans.put(name, bean);
161 		}
162 
163 		if (bean instanceof ConversationBindingListener)
164 		{
165 			((ConversationBindingListener) bean).valueBound(
166 				new ConversationBindingEvent(this, name));
167 		}
168 	}
169 
170 	/***
171 	 * Assert the conversation is valid.
172 	 * 
173 	 * Throws IllegalStateException if this conversation has been destroyed;
174 	 * see method setInvalid.
175 	 */
176 	protected void checkValid()
177 	{
178 		if (isInvalid())
179 		{
180 			throw new IllegalStateException("conversation '" + getName() + "' closed");
181 		}
182 	}
183 
184 	/***
185 	 * Return the name of this conversation.
186 	 * <p>
187 	 * A conversation name is unique within a conversation context.
188 	 */
189 	public String getName()
190 	{
191 		return name;
192 	}
193 
194 	/***
195 	 * Return the factory that created this conversation.
196 	 * <p>
197 	 * Note that this factory will have set the initial aspects of this factory, which
198 	 * configure such things as the lifetime (flash, manual, etc) and conversation
199 	 * timeout properties.
200 	 */
201 	public ConversationFactory getFactory()
202 	{
203 		return factory;
204 	}
205 
206 	/***
207 	 * Invalidate (end) the conversation.
208 	 * <p>
209 	 * If the conversation is currently active (ie the current call stack contains an object that
210 	 * belongs to this conversation) then the conversation will just queue the object for later
211 	 * destruction. Calls to methods like ConversationManager.getConversation(...) may still
212 	 * return this object, and it will continue to function as a normal instance.
213 	 * <p>
214 	 * Only when the conversation is no longer active will the conversation (and the beans
215 	 * it contains) actually be marked as invalid ("destroyed"). Once the conversation has been
216 	 * destroyed, the ConversationManager will discard all references to it, meaning it will no
217 	 * longer be accessable via lookups like ConversationManager.getConversation(). If something
218 	 * does still have a reference to a destroyed conversation, then invoking almost any method
219 	 * on that object will throw an IllegalStateException. In particular, adding a bean to the
220 	 * conversation (invoking addAttribute) is not allowed.
221 	 */
222 	public void invalidate()
223 	{
224 		if (!isActive())
225 		{
226 			destroy();
227 		}
228 		else
229 		{
230 			queueInvalid = true;
231 
232 			if (log.isDebugEnabled())
233 			{
234 				log.debug("conversation '" + name + "' queued for destroy.");
235 			}
236 		}
237 	}
238 
239 	/***
240 	 * Invalidate/End and restart the conversation.
241 	 * <p>
242 	 * This conversation object is immediately "destroyed" (see comments for method
243 	 * invalidate), and a new instance is registered with the conversation manager
244 	 * using the same name. The new instance is returned from this method.
245 	 * <p>
246 	 * Any code holding a reference to the old conversation instance will receive
247 	 * an IllegalStateException when calling almost any method on that instance.
248 	 *
249 	 * @returns the new conversation
250 	 */
251 	public Conversation invalidateAndRestart()
252 	{
253 		String conversationName = getName();
254 		ConversationFactory factory = getFactory();
255 
256 		destroy();
257 
258 		return conversationContext.startConversation(conversationName, factory);
259 	}
260 
261 	/***
262 	 * Return true if the conversation is invalid, ie should not be used.
263 	 */
264 	public boolean isInvalid()
265 	{
266 		return invalid;
267 	}
268 
269 	/***
270 	 * Return true if the conversation has been queued to be invalidated.
271 	 */
272 	boolean isQueueInvalid()
273 	{
274 		return queueInvalid;
275 	}
276 
277 	/***
278 	 * Destroy the conversation.
279 	 * <ul>
280 	 * <li>inform all beans implementing the {@link ConversationBindingListener} about the conversation end</li>
281 	 * <li>free all beans</li>
282 	 * </ul>
283 	 */
284 	protected void destroy()
285 	{
286 		if (log.isDebugEnabled())
287 		{
288 			log.debug("destroy conversation:" + name);
289 		}
290 
291 		synchronized(conversationContext)
292 		{
293 			String[] beanNames = (String[]) beans.keySet().toArray(new String[beans.size()]);
294 			for (int i = 0; i< beanNames.length; i++)
295 			{
296 				removeAttribute(beanNames[i]);
297 			}
298 		}
299 
300 		conversationContext.removeConversation(getName());
301 
302 		invalid = true;
303 	}
304 
305 	/***
306 	 * Check if this conversation holds a specific attribute (ie has a specific
307 	 * named managed bean instance).
308 	 */
309 	public boolean hasAttribute(String name)
310 	{
311 		synchronized(conversationContext)
312 		{
313 			return beans.containsKey(name);
314 		}
315 	}
316 
317 	/***
318 	 * Get a specific attribute, ie a named managed bean.
319 	 */
320 	public Object getAttribute(String name)
321 	{
322 		synchronized(conversationContext)
323 		{
324 			return beans.get(name);
325 		}
326 	}
327 
328 	/***
329 	 * Remove a bean from the conversation.
330 	 * 
331 	 * <p>This will fire a {@link ConversationBindingEvent} if the bean implements the
332 	 * {@link ConversationBindingListener} interface.</p>
333 	 */
334 	public Object removeAttribute(String name)
335 	{
336 		synchronized(conversationContext)
337 		{
338 			Object bean = beans.remove(name);
339 			if (bean instanceof ConversationBindingListener)
340 			{
341 				((ConversationBindingListener) bean).valueUnbound(
342 					new ConversationBindingEvent(this, name));
343 			}
344 			return bean;
345 		}
346 	}
347 
348 	/***
349 	 * Get the current conversation.
350 	 *
351 	 * @return The conversation object associated with the nearest object in the call-stack that
352 	 * is configured to be in a conversation. Null is returned when no object in the call-stack
353 	 * is in a conversation.
354 	 */
355 	public static Conversation getCurrentInstance()
356 	{
357 		CurrentConversationInfo conversation = getCurrentInstanceInfo();
358 		if (conversation != null)
359 		{
360 			return conversation.getConversation();
361 		}
362 
363 		return null;
364 	}
365 
366 	/***
367 	 * Sets info about the current conversation instance.
368 	 * <p>
369 	 * This method is only expected to be called by CurrentConversationAdvice.invoke,
370 	 * which ensures that the current instance is reset to null as soon as no bean
371 	 * in the call-stack is within a conversation.
372 	 */
373 	static void setCurrentInstance(CurrentConversationInfo conversation)
374 	{
375 		CURRENT_CONVERSATION.set(conversation);
376 	}
377 
378 	/***
379 	 * Returns the info about the current conversation
380 	 */
381 	static CurrentConversationInfo getCurrentInstanceInfo()
382 	{
383 		CurrentConversationInfo conversationInfo = (CurrentConversationInfo) CURRENT_CONVERSATION.get();
384 		if (conversationInfo != null && conversationInfo.getConversation() != null)
385 		{
386 			conversationInfo.getConversation().touch();
387 			return conversationInfo;
388 		}
389 
390 		return null;
391 	}
392 
393 	/***
394 	 * Increase one to the "conversation active" counter.
395 	 *
396 	 * This is called when a method is invoked on a bean that is within this conversation.
397 	 * When the method returns, leaveConversation is invoked. The result is that the count
398 	 * is greater than zero whenever there is a bean belonging to this conversation on
399 	 * the callstack.
400 	 */
401 	void enterConversation()
402 	{
403 		synchronized (activeCountMutex)
404 		{
405 			activeCount++;
406 		}
407 	}
408 
409 	/***
410 	 * decrease one from the "conversation active" counter
411 	 */
412 	void leaveConversation()
413 	{
414 		synchronized (activeCountMutex)
415 		{
416 			activeCount--;
417 		}
418 	}
419 
420 	/***
421 	 * check if the conversation is active
422 	 */
423 	private boolean isActive()
424 	{
425 		synchronized (activeCountMutex)
426 		{
427 			return activeCount > 0;
428 		}
429 	}
430 
431 	ConversationAspects getAspects()
432 	{
433 		return conversationAspects;
434 	}
435 
436 	/***
437 	 * Get the aspect corresponding to the given class.
438 	 * 
439 	 * @return null if such an aspect has not been attached to this conversation
440 	 */
441 	public ConversationAspect getAspect(Class conversationAspectClass)
442 	{
443 		return conversationAspects.getAspect(conversationAspectClass);
444 	}
445 
446 	/***
447 	 * Add an Aspect to this conversation. 
448 	 * 
449 	 * See class ConversationAspects for further details.
450 	 */
451 	public void addAspect(ConversationAspect aspect)
452 	{
453 		conversationAspects.addAspect(aspect);
454 	}
455 }