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