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