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>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      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 (access, 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      * @return 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 }