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  import org.apache.myfaces.orchestra.lib.OrchestraException;
25  import org.apache.myfaces.orchestra.lib._ReentrantLock;
26  
27  import java.util.Arrays;
28  import java.util.HashMap;
29  import java.util.Iterator;
30  import java.util.Map;
31  import java.util.TreeMap;
32  import java.util.Collection;
33  
34  /**
35   * A ConversationContext is a container for a set of conversations.
36   * <p>
37   * Normally there is only one ConversationContext per http session. However there can
38   * be multiple instances if the user has multiple concurrent windows open into the same
39   * webapp, using the ox:separateConversationContext or other similar mechanism.
40   * <p>
41   * Like the conversation class, a context can also have a timeout which will cause it
42   * to be ended automatically if not accessed within the given period.
43   */
44  public class ConversationContext
45  {
46      private final Log log = LogFactory.getLog(ConversationContext.class);
47  
48      // This id is attached as a query parameter to every url rendered in a page
49      // (forms and links) so that if that url is invoked then the request will
50      // cause the same context to be used from the user's http session.
51      //
52      // This value is never null, but an Object is used to store it rather than
53      // a primitive long because it is used as a key into a collection of
54      // conversation contexts, and using an object here saves wrapping this
55      // value in a new object instance every time it must be used as a key.
56      private final Long id;
57  
58      // See addAttribute
59      private final Map attributes = new TreeMap();
60  
61      // the parent conversation context
62      private final ConversationContext parent;
63  
64      // The conversations held by this context, keyed by conversation name.
65      private final Map conversations = new TreeMap();
66  
67      /**
68       * A name associated with this context
69       */
70      private String name;
71  
72      // time at which this was last accessed, used for timeouts.
73      private long lastAccess;
74  
75      // default timeout for contexts: 30 minutes.
76      private long timeoutMillis = 30 * 60 * 1000;
77  
78      private final _ReentrantLock lock = new _ReentrantLock();
79  
80      // Map of all child contexts of this context, keyed by child.id
81      private Map childContexts = new HashMap();
82  
83      /**
84       * Constructor.
85       */
86      protected ConversationContext(long id)
87      {
88          this(null, id);
89      }
90  
91      /**
92       * Constructor.
93       * 
94       * @since 1.2
95       */
96      protected ConversationContext(ConversationContext parent, long id)
97      {
98          this.parent = parent;
99          this.id = Long.valueOf(id);
100 
101         if (parent != null)
102         {
103             parent.addChild(this);
104         }
105 
106         touch();
107     }
108 
109     /**
110      * Get the name associated to this context.
111      * 
112      * @since 1.2
113      */
114     public String getName()
115     {
116         return name;
117     }
118 
119     /**
120      * Assign a name to this context.
121      * 
122      * @since 1.2
123      */
124     public void setName(String name)
125     {
126         this.name = name;
127     }
128 
129     /**
130      * The conversation context id, unique within the current http session.
131      */
132     public long getId()
133     {
134         return id.longValue();
135     }
136 
137     /**
138      * The conversation context id, unique within the current http session.
139      */
140     public Long getIdAsLong()
141     {
142         return id;
143     }
144 
145     /**
146      * Return the parent conversation context (if any).
147      * 
148      * @since 1.2
149      */
150     public ConversationContext getParent()
151     {
152         return parent;
153     }
154 
155     /**
156      * @since 1.3
157      */
158     public void addChild(ConversationContext context)
159     {
160         childContexts.put(context.getIdAsLong(), context);
161     }
162 
163     /**
164      * @since 1.4
165      */
166     protected Collection getChildren()
167     {
168         return childContexts.values();
169     }
170 
171     /**
172      * @since 1.3
173      */
174     public void removeChild(ConversationContext context)
175     {
176         Object o = childContexts.remove(context.getIdAsLong());
177         if (o != context)
178         {
179             // Sanity check failed: o is null, or o is a different object.
180             // In either case, something is very wrong.
181             throw new OrchestraException("Invalid call of removeChild");
182         }
183     }
184 
185     /**
186      * @since 1.3
187      */
188     public boolean hasChildren()
189     {
190         return !childContexts.isEmpty();
191     }
192 
193     /**
194      * Mark this context as having been used.
195      */
196     protected void touch()
197     {
198         lastAccess = System.currentTimeMillis();
199 
200         if (getParent() != null)
201         {
202             getParent().touch();
203         }
204     }
205 
206     /**
207      * The system time in millis when this conversation has been accessed last.
208      */
209     public long getLastAccess()
210     {
211         return lastAccess;
212     }
213 
214     /**
215      * Get the timeout after which this context will be closed.
216      *
217      * @see #setTimeout
218      */
219     public long getTimeout()
220     {
221         return timeoutMillis;
222     }
223 
224     /**
225      * Set the timeout after which this context will be closed.
226      * <p>
227      * A value of -1 means no timeout checking.
228      */
229     public void setTimeout(long timeoutMillis)
230     {
231         this.timeoutMillis = timeoutMillis;
232     }
233 
234     /**
235      * Invalidate all conversations within this context.
236      * 
237      * @deprecated Use the "invalidate" method instead.
238      */
239     protected void clear()
240     {
241         invalidate();
242     }
243 
244     /**
245      * Invalidate all conversations within this context.
246      *
247      * @since 1.3
248      */
249     protected void invalidate()
250     {
251         synchronized (this)
252         {
253             Conversation[] convArray = new Conversation[conversations.size()];
254             conversations.values().toArray(convArray);
255 
256             for (int i = 0; i < convArray.length; i++)
257             {
258                 Conversation conversation = convArray[i];
259                 conversation.invalidate();
260             }
261 
262             conversations.clear();
263         }
264     }
265 
266     /**
267      * Start a conversation if not already started.
268      */
269     protected Conversation startConversation(String name, ConversationFactory factory)
270     {
271         synchronized (this)
272         {
273             touch();
274             Conversation conversation = (Conversation) conversations.get(name);
275             if (conversation == null)
276             {
277                 conversation = factory.createConversation(this, name);
278 
279                 conversations.put(name, conversation);
280             }
281             return conversation;
282         }
283     }
284 
285     /**
286      * Remove the conversation from this context.
287      *
288      * <p>Notice: It is assumed that the conversation has already been invalidated.</p>
289      */
290     protected void removeConversation(Conversation conversation)
291     {
292         synchronized (this)
293         {
294             touch();
295             conversations.remove(conversation.getName());
296         }
297     }
298 
299     /**
300      * Remove the conversation with the given name from this context.
301      *
302      * <p>Notice: Its assumed that the conversation has already been invalidated</p>
303      */
304     protected void removeConversation(String name)
305     {
306         synchronized (this)
307         {
308             touch();
309             Conversation conversation = (Conversation) conversations.get(name);
310             if (conversation != null)
311             {
312                 removeConversation(conversation);
313             }
314         }
315     }
316 
317     /**
318      * Return true if there are one or more conversations in this context.
319      */
320     protected boolean hasConversations()
321     {
322         synchronized (this)
323         {
324             touch();
325             return conversations.size() > 0;
326         }
327     }
328 
329     /**
330      * Check if the given conversation exists.
331      */
332     protected boolean hasConversation(String name)
333     {
334         synchronized (this)
335         {
336             touch();
337             return conversations.get(name) != null;
338         }
339     }
340 
341     /**
342      * Get a conversation by name.
343      * <p>
344      * This looks only in the current context, not in any child contexts.
345      */
346     protected Conversation getConversation(String name)
347     {
348         synchronized (this)
349         {
350             touch();
351 
352             Conversation conv = (Conversation) conversations.get(name);
353             if (conv != null)
354             {
355                 conv.touch();
356             }
357 
358             return conv;
359         }
360     }
361 
362     /**
363      * Iterates over all the conversations in this context.
364      * <p>
365      * This does not include conversations in parent contexts.
366      *
367      * @return An iterator over a copy of the conversation list. It is safe to remove objects from
368      * the conversation list while iterating, as the iterator refers to a different collection.
369      */
370     public Iterator iterateConversations()
371     {
372         synchronized (this)
373         {
374             touch();
375 
376             Conversation[] convs = (Conversation[]) conversations.values().toArray(
377                     new Conversation[conversations.size()]);
378             return Arrays.asList(convs).iterator();
379         }
380     }
381 
382     /**
383      * Check the timeout for every conversation in this context.
384      * <p>
385      * This method does not check the timeout for this context object itself.
386      */
387     protected void checkConversationTimeout()
388     {
389         synchronized (this)
390         {
391             Conversation[] convArray = new Conversation[conversations.size()];
392             conversations.values().toArray(convArray);
393 
394             for (int i = 0; i < convArray.length; i++)
395             {
396                 Conversation conversation = convArray[i];
397 
398                 ConversationTimeoutableAspect timeoutAspect =
399                     (ConversationTimeoutableAspect)
400                         conversation.getAspect(ConversationTimeoutableAspect.class);
401 
402                 if (timeoutAspect != null && timeoutAspect.isTimeoutReached())
403                 {
404                     if (log.isDebugEnabled())
405                     {
406                         log.debug("end conversation due to timeout: " + conversation.getName());
407                     }
408 
409                     conversation.invalidate();
410                 }
411             }
412         }
413     }
414 
415     /**
416      * Add an attribute to the conversationContext.
417      * <p>
418      * A context provides a map into which any arbitrary objects can be stored. It
419      * isn't a major feature of the context, but can occasionally be useful.
420      */
421     public void setAttribute(String name, Object attribute)
422     {
423         synchronized(attributes)
424         {
425             attributes.remove(name);
426             attributes.put(name, attribute);
427         }
428     }
429 
430     /**
431      * Check if this conversationContext holds a specific attribute.
432      */
433     public boolean hasAttribute(String name)
434     {
435         synchronized(attributes)
436         {
437             return attributes.containsKey(name);
438         }
439     }
440 
441     /**
442      * Get a specific attribute.
443      */
444     public Object getAttribute(String name)
445     {
446         synchronized(attributes)
447         {
448             return attributes.get(name);
449         }
450     }
451 
452     /**
453      * Remove an attribute from the conversationContext.
454      */
455     public Object removeAttribute(String name)
456     {
457         synchronized(attributes)
458         {
459             return attributes.remove(name);
460         }
461     }
462 
463     /**
464      * Block until no other thread has this instance marked as reserved, then
465      * mark the object as reserved for this thread.
466      * <p>
467      * It is safe to call this method multiple times.
468      * <p>
469      * If this method is called, then an equal number of calls to
470      * unlockForCurrentThread <b>MUST</b> made, or this context object
471      * will remain locked until the http session times out.
472      * <p>
473      * Note that this method may be called very early in the request processing
474      * lifecycle, eg before a FacesContext exists for a JSF request.
475      *
476      * @since 1.1
477      */
478     public void lockInterruptablyForCurrentThread() throws InterruptedException
479     {
480         if (log.isDebugEnabled())
481         {
482             log.debug("Locking context " + this.id);
483         }
484         lock.lockInterruptibly();
485     }
486 
487     /**
488      * Block until no other thread has this instance marked as reserved, then
489      * mark the object as reserved for this thread.
490      * <p>
491      * Note that this method may be called very late in the request processing
492      * lifecycle, eg after a FacesContext has been destroyed for a JSF request.
493      *
494      * @since 1.1
495      */
496     public void unlockForCurrentThread()
497     {
498         if (log.isDebugEnabled())
499         {
500             log.debug("Unlocking context " + this.id);
501         }
502         lock.unlock();
503     }
504 
505     /**
506      * Return true if this object is currently locked by the calling thread.
507      *
508      * @since 1.1
509      */
510     public boolean isLockedForCurrentThread()
511     {
512         return lock.isHeldByCurrentThread();
513     }
514 
515     /**
516      * Get the root conversation context this conversation context is
517      * associated with.
518      * <p>
519      * This is equivalent to calling getParent repeatedly until a context
520      * with no parent is found.
521      * 
522      * @since 1.2
523      */
524     public ConversationContext getRoot()
525     {
526         ConversationContext cctx = this;
527         while (cctx != null && cctx.getParent() != null)
528         {
529             cctx = getParent();
530         }
531 
532         return cctx;
533     }
534 }