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