1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
68
69 private final ConversationFactory factory;
70
71
72 private final Map beans = new TreeMap();
73
74
75
76 private final ConversationContext conversationContext;
77
78
79 private ConversationAspects conversationAspects = new ConversationAspects();
80
81
82 private boolean invalid = false;
83
84
85 private boolean queueInvalid = false;
86
87
88
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 }