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 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 }