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 org.apache.commons.logging.Log;
23 import org.apache.commons.logging.LogFactory;
24
25 import java.util.Map;
26 import java.util.TreeMap;
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>A conversation instance is typically destroyed:
47 * <ul>
48 * <li>At the end of a request when it are marked as access-scoped 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 // The name of this conversation
66 private final String name;
67
68 // The factory that created this conversation instance; needed
69 // when restarting the conversation.
70 private final ConversationFactory factory;
71
72 // The parent context to which this conversation belongs. This is needed
73 // when restarting the conversation.
74 private final ConversationContext conversationContext;
75
76 // An object that can bind arbitrary objects to this conversation so that all calls to
77 // methods on the target object cause this conversation to become the active conversation,
78 // and for any other optional "entry actions" (Advices) to be applied.
79 private ConversationBinder binder;
80
81 // The set of managed beans that are associated with this conversation.
82 private Map beans = new TreeMap();
83
84 // See addAspect.
85 private ConversationAspects conversationAspects = new ConversationAspects();
86
87 // Is this object usable, or "destroyed"?
88 private boolean invalid = false;
89
90 // Is this object going to be destroyed as soon as it is no longer "active"?
91 private boolean queueInvalid = false;
92
93 // system timestamp in milliseconds at which this object was last accessed;
94 // see method touch().
95 private long lastAccess;
96
97 private Object activeCountMutex = new Object();
98 private int activeCount;
99
100 public Conversation(ConversationContext conversationContext, String name, ConversationFactory factory)
101 {
102 this.conversationContext = conversationContext;
103 this.name = name;
104 this.factory = factory;
105
106 if (log.isDebugEnabled())
107 {
108 log.debug("start conversation:" + name);
109 }
110
111 touch();
112 }
113
114 /**
115 * Define the (optional) binder used by this instance in method bind(Object).
116 * <p>
117 * Expected to be called by code that creates instances of this type immediately
118 * after the constructor is invoked. See bind(Object) for more details.
119 *
120 * @since 1.3
121 */
122 public void setBinder(ConversationBinder binder)
123 {
124 this.binder = binder;
125 }
126
127 /**
128 * Mark this conversation as having been used at the current time.
129 * <p>
130 * Conversations can have "timeouts" associated with them, so that when a user stops
131 * a conversation and goes off to work on some other part of the webapp then the
132 * conversation's memory can eventually be reclaimed.
133 * <p>
134 * Whenever user code causes this conversation object to be looked up and returned,
135 * this "touch" method is invoked to indicate that the conversation is in use. Direct
136 * conversation lookups by user code can occur, but the most common access is expected
137 * to be via an EL expression which a lookup of a bean that is declared as being in
138 * conversation scope. The bean lookup causes the corresponding conversation to be
139 * looked up, which triggers this method.
140 */
141 protected void touch()
142 {
143 lastAccess = System.currentTimeMillis();
144 }
145
146 /**
147 * The system time in millis when this conversation has been accessed last
148 */
149 public long getLastAccess()
150 {
151 return lastAccess;
152 }
153
154 /**
155 * Add the given bean to the conversation scope.
156 *
157 * <p>This will fire a {@link ConversationBindingEvent} on the bean parameter
158 * object if the bean implements the {@link ConversationBindingListener}
159 * interface</p>
160 *
161 * <p>Note that any object can be stored into the conversation; it is not
162 * limited to managed beans declared in a configuration file. This
163 * feature is not expected to be heavily used however; most attributes of
164 * a conversation are expected to be externally-declared "managed beans".</p>
165 */
166 public void setAttribute(String name, Object bean)
167 {
168 checkValid();
169
170 synchronized(conversationContext)
171 {
172 removeAttribute(name);
173
174 if (log.isDebugEnabled())
175 {
176 log.debug("put bean to conversation:" + name + "(bean=" + bean + ")");
177 }
178
179 beans.put(name, bean);
180 }
181
182 if (bean instanceof ConversationBindingListener)
183 {
184 ((ConversationBindingListener) bean).valueBound(
185 new ConversationBindingEvent(this, name));
186 }
187 }
188
189 /**
190 * Assert the conversation is valid.
191 *
192 * Throws IllegalStateException if this conversation has been destroyed;
193 * see method setInvalid.
194 */
195 protected void checkValid()
196 {
197 if (isInvalid())
198 {
199 throw new IllegalStateException("conversation '" + getName() + "' closed");
200 }
201 }
202
203 /**
204 * Return the name of this conversation.
205 * <p>
206 * A conversation name is unique within a conversation context.
207 */
208 public String getName()
209 {
210 return name;
211 }
212
213 /**
214 * Return the factory that created this conversation.
215 * <p>
216 * Note that this factory will have set the initial aspects of this factory, which
217 * configure such things as the lifetime (access, manual, etc) and conversation
218 * timeout properties.
219 */
220 public ConversationFactory getFactory()
221 {
222 return factory;
223 }
224
225 /**
226 * Invalidate (end) the conversation.
227 * <p>
228 * If the conversation is currently active (ie the current call stack contains an object that
229 * belongs to this conversation) then the conversation will just queue the object for later
230 * destruction. Calls to methods like ConversationManager.getConversation(...) may still
231 * return this object, and it will continue to function as a normal instance.
232 * <p>
233 * Only when the conversation is no longer active will the conversation (and the beans
234 * it contains) actually be marked as invalid ("destroyed"). Once the conversation has been
235 * destroyed, the ConversationManager will discard all references to it, meaning it will no
236 * longer be accessable via lookups like ConversationManager.getConversation(). If something
237 * does still have a reference to a destroyed conversation, then invoking almost any method
238 * on that object will throw an IllegalStateException. In particular, adding a bean to the
239 * conversation (invoking addAttribute) is not allowed.
240 */
241 public void invalidate()
242 {
243 if (!isActive())
244 {
245 destroy();
246 }
247 else
248 {
249 queueInvalid = true;
250
251 if (log.isDebugEnabled())
252 {
253 log.debug("conversation '" + name + "' queued for destroy.");
254 }
255 }
256 }
257
258 /**
259 * Invalidate/End and restart the conversation.
260 * <p>
261 * This conversation object is immediately "destroyed" (see comments for method
262 * invalidate), and a new instance is registered with the conversation manager
263 * using the same name. The new instance is returned from this method.
264 * <p>
265 * Any code holding a reference to the old conversation instance will receive
266 * an IllegalStateException when calling almost any method on that instance.
267 *
268 * @return the new conversation
269 */
270 public Conversation invalidateAndRestart()
271 {
272 String conversationName = getName();
273 ConversationFactory factory = getFactory();
274
275 destroy();
276
277 return conversationContext.startConversation(conversationName, factory);
278 }
279
280 /**
281 * Return true if the conversation is invalid, ie should not be used.
282 */
283 public boolean isInvalid()
284 {
285 return invalid;
286 }
287
288 /**
289 * Return true if the conversation has been queued to be invalidated.
290 */
291 boolean isQueueInvalid()
292 {
293 return queueInvalid;
294 }
295
296 /**
297 * Destroy the conversation.
298 * <ul>
299 * <li>inform all beans implementing the {@link ConversationBindingListener} about the conversation end</li>
300 * <li>free all beans</li>
301 * </ul>
302 * <p>
303 * After return from this method, this conversation object's invalid flag is set and the map of
304 * beans associated with this conversation is empty. In addition, the parent context no longer
305 * holds a reference to this conversation.
306 */
307 protected void destroy()
308 {
309 if (log.isDebugEnabled())
310 {
311 log.debug("destroy conversation:" + name);
312 }
313
314 synchronized(conversationContext)
315 {
316 String[] beanNames = (String[]) beans.keySet().toArray(new String[beans.size()]);
317 for (int i = 0; i< beanNames.length; i++)
318 {
319 removeAttribute(beanNames[i]);
320 }
321 }
322
323 conversationContext.removeConversation(getName());
324
325 invalid = true;
326 }
327
328 /**
329 * Check if this conversation holds a specific attribute (ie has a specific
330 * named managed bean instance).
331 */
332 public boolean hasAttribute(String name)
333 {
334 synchronized(conversationContext)
335 {
336 return beans.containsKey(name);
337 }
338 }
339
340 /**
341 * Get a specific attribute, ie a named managed bean.
342 */
343 public Object getAttribute(String name)
344 {
345 synchronized(conversationContext)
346 {
347 return beans.get(name);
348 }
349 }
350
351 /**
352 * Remove a bean from the conversation.
353 *
354 * <p>This will fire a {@link ConversationBindingEvent} if the bean implements the
355 * {@link ConversationBindingListener} interface.</p>
356 */
357 public Object removeAttribute(String name)
358 {
359 synchronized(conversationContext)
360 {
361 Object bean = beans.remove(name);
362 if (bean instanceof ConversationBindingListener)
363 {
364 ((ConversationBindingListener) bean).valueUnbound(
365 new ConversationBindingEvent(this, name));
366 }
367 return bean;
368 }
369 }
370
371 /**
372 * Get the current conversation.
373 *
374 * @return The conversation object associated with the nearest object in the call-stack that
375 * is configured to be in a conversation.<br />
376 * If there is no object in the call-stack the system will lookup the single conversation
377 * bound to the conversationContext.<br />
378 * If not found, null will be returned.
379 */
380 public static Conversation getCurrentInstance()
381 {
382 CurrentConversationInfo conversation = getCurrentInstanceInfo();
383 if (conversation != null)
384 {
385 return conversation.getConversation();
386 }
387
388 return null;
389 }
390
391 /**
392 * Sets info about the current conversation instance.
393 * <p>
394 * This method is only expected to be called by CurrentConversationAdvice.invoke,
395 * which ensures that the current instance is reset to null as soon as no bean
396 * in the call-stack is within a conversation.
397 */
398 static void setCurrentInstance(CurrentConversationInfo conversation)
399 {
400 CURRENT_CONVERSATION.set(conversation);
401 }
402
403 /**
404 * Returns the info about the current conversation
405 */
406 static CurrentConversationInfo getCurrentInstanceInfo()
407 {
408 CurrentConversationInfo conversationInfo = (CurrentConversationInfo) CURRENT_CONVERSATION.get();
409 if (conversationInfo != null && conversationInfo.getConversation() != null)
410 {
411 conversationInfo.getConversation().touch();
412 return conversationInfo;
413 }
414
415 return null;
416 }
417
418 /**
419 * Increase one to the "conversation active" counter.
420 * <p>
421 * This is called when a method is invoked on a bean that is within this conversation.
422 * When the method returns, leaveConversation is invoked. The result is that the count
423 * is greater than zero whenever there is a bean belonging to this conversation on
424 * the callstack.
425 * <p>
426 * This method throws IllegalStateException if it is called on a conversation that has
427 * been destroyed.
428 */
429 void enterConversation()
430 {
431 checkValid();
432
433 synchronized (activeCountMutex)
434 {
435 activeCount++;
436 }
437 }
438
439 /**
440 * decrease one from the "conversation active" counter
441 */
442 void leaveConversation()
443 {
444 synchronized (activeCountMutex)
445 {
446 activeCount--;
447 }
448 }
449
450 /**
451 * check if the conversation is active
452 */
453 private boolean isActive()
454 {
455 synchronized (activeCountMutex)
456 {
457 return activeCount > 0;
458 }
459 }
460
461 ConversationAspects getAspects()
462 {
463 return conversationAspects;
464 }
465
466 /**
467 * Get the aspect corresponding to the given class.
468 *
469 * @return null if such an aspect has not been attached to this conversation
470 */
471 public ConversationAspect getAspect(Class conversationAspectClass)
472 {
473 return conversationAspects.getAspect(conversationAspectClass);
474 }
475
476 /**
477 * Add an Aspect to this conversation.
478 *
479 * See class ConversationAspects for further details.
480 */
481 public void addAspect(ConversationAspect aspect)
482 {
483 conversationAspects.addAspect(aspect);
484 }
485
486 /**
487 * Get direct access to the beans map.
488 * <p>
489 * This method is only intended for use by subclasses that manipulate
490 * the beans map in unusual ways. In general, it is better to use the
491 * setAttribute/removeAttribute methods rather than accessing beans via
492 * this map. Adding/removing entries in this map will not trigger the
493 * usual callbacks on the bean objects themselves.
494 *
495 * @since 1.2
496 */
497 protected Map getBeans()
498 {
499 synchronized(conversationContext)
500 {
501 return beans;
502 }
503 }
504
505 /**
506 * Replace the current beans map.
507 * <p>
508 * @see #getBeans()
509 * @since 1.2
510 */
511 protected void setBeans(Map beans)
512 {
513 synchronized(conversationContext)
514 {
515 this.beans = beans;
516 }
517 }
518
519 /**
520 * Return a proxy object that "binds" the specified instance to this conversation.
521 * <p>
522 * Whenever a method is executed on the proxy, this conversation is "entered" before the method
523 * is invoked on the actual instance, as if the specified instance were a bean that was defined
524 * in the dependency-injection framework as belonging to this conversation.
525 * <p>
526 * The specified bean is NOT added to the set of beans belonging to the conversation, ie its
527 * lifecycle is still independent of the conversation.
528 * <p>
529 * The returned proxy is bound to a specific Conversation instance, so it should not be stored for
530 * a long time; if the conversation is "invalidated" the proxy will continue to reference the original
531 * conversation instance meaning that invoking the proxy would use the "stale" conversation rather than
532 * a new instance. It also means that memory allocated to the conversation will not be recycled, although
533 * this is not too serious as the invalidated conversation will be empty of beans. This limitation on
534 * the lifetime of the returned proxy is not an issue for many of the uses of this method; in particular,
535 * when wrapping objects returned by property getters for the use of EL expressions this is fine as the
536 * proxy is only used during the scope of the EL expression execution. If a proxy is used after the
537 * conversation it is bound to has been invalidated then an IllegalStateException will be thrown.
538 * <p>
539 * This method is useful when a conversation-scoped object creates an object instance via new() or via
540 * calling some other library, and then wants all operations on that object to run within its own
541 * conversation. In particular, when a backing-bean returns a persistent object that has been loaded
542 * via a DAO class, it is often desirable for all methods on the persistent object to run within
543 * the backing-bean's persistence context, ie within the same persistence context set up which
544 * existed when the DAO class loaded the instance. For example, a JSF EL expression may retrieve
545 * a persistent object from a backing bean then navigate through its properties; walking lazy
546 * relations in this way will fail unless calls to methods of the persistent object cause the
547 * correct persistence-context to be set up.
548 * <p>
549 * This method is theoretically an optional operation; the orchestra adapter layer for some specific
550 * dependency-injection frameworks might choose not to support this, in which case an
551 * UnsupportedOperationException will be thrown. The default Orchestra-Spring integration
552 * certainly does support it.
553 * <p>
554 * It initially seems reasonable for Orchestra to also provide a variant of this method that
555 * returns a "scoped proxy" object that looks up the most recent version of the conversation
556 * by name and then runs the bound object in the context of that conversation instance. This
557 * would mean that a proxy would never throw an IllegalStateException due to its conversation
558 * having been invalidated. However this is not actually very useful. The primary use for this
559 * method is expected to be wrapping of persistent objects returned by JPA, Hibernate or similar.
560 * In this case the point is to access the object using the conversation's "persistence context";
561 * a new conversation will have a new persistence context instance, not the one the object needs.
562 *
563 * @throw {@link UnsupportedOperationException}
564 * @since 1.3
565 */
566 public Object bind(Object instance)
567 {
568 if (binder == null)
569 {
570 throw new UnsupportedOperationException("No beanBinder instance");
571 }
572 return binder.bind(instance);
573 }
574 }