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.frameworkAdapter.FrameworkAdapter;
25  import org.apache.myfaces.orchestra.requestParameterProvider.RequestParameterProviderManager;
26  
27  import java.io.IOException;
28  import java.io.ObjectStreamException;
29  import java.io.Serializable;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.Iterator;
33  import java.util.Map;
34  
35  /**
36   * Deals with the various conversation contexts in the current session.
37   * <p>
38   * A new conversation context will be created if the servlet request did
39   * not specify an existing conversation context id.
40   * <p>
41   * At the current time, this object does not serialize well. Any attempt to serialize
42   * this object (including any serialization of the user session) will just cause it
43   * to be discarded.
44   * <p>
45   * TODO: fix serialization issues.
46   */
47  public class ConversationManager implements Serializable
48  {
49      private static final long serialVersionUID = 1L;
50  
51      final static String CONVERSATION_CONTEXT_PARAM = "conversationContext";
52  
53      private final static String CONVERSATION_MANAGER_KEY = "org.apache.myfaces.ConversationManager";
54      private final static String CONVERSATION_CONTEXT_REQ = "org.apache.myfaces.ConversationManager.conversationContext";
55  
56      private static final Iterator EMPTY_ITERATOR = Collections.EMPTY_LIST.iterator();
57  
58      private final Log log = LogFactory.getLog(ConversationManager.class);
59  
60      /**
61       * Used to generate a unique id for each "window" that a user has open
62       * on the same webapp within the same HttpSession. Note that this is a
63       * property of an object stored in the session, so will correctly
64       * migrate from machine to machine along with a distributed HttpSession.
65       *
66       */
67      private long nextConversationContextId = 1;
68  
69      // This member must always be accessed with a lock held on the parent ConverstationManager instance;
70      // a HashMap is not thread-safe and this class must be thread-safe.
71      private final Map conversationContexts = new HashMap();
72  
73      protected ConversationManager()
74      {
75      }
76  
77      /**
78       * Get the conversation manager. This creates a new one if none exists.
79       */
80      public static ConversationManager getInstance()
81      {
82          return getInstance(true);
83      }
84  
85      /**
86       * Get the conversation manager.
87       * <p>
88       * When create is true, an instance is always returned; one is
89       * created if none currently exists for the current user session.
90       * <p>
91       * When create is false, null is returned if no instance yet
92       * exists for the current user session.
93       */
94      public static ConversationManager getInstance(boolean create)
95      {
96          FrameworkAdapter frameworkAdapter = FrameworkAdapter.getCurrentInstance();
97          if (frameworkAdapter == null)
98          {
99              if (!create)
100             {
101                 // if we should not created one, it doesn't matter if there is no
102                 // FrameworkAdapter available.
103                 return null;
104             }
105             else
106             {
107                 throw new IllegalStateException("FrameworkAdapter not found");
108             }
109         }
110 
111         ConversationManager conversationManager = (ConversationManager) frameworkAdapter.getSessionAttribute(
112                 CONVERSATION_MANAGER_KEY);
113         if (conversationManager == null && create)
114         {
115             // TODO: do not call new directly here, as it makes it impossible to configure
116             // an alternative ConversationManager instance. This is IOC and test unfriendly.
117             conversationManager = new ConversationManager();
118 
119             // initialize environmental systems
120             RequestParameterProviderManager.getInstance().register(new ConversationRequestParameterProvider());
121 
122             // set mark
123             FrameworkAdapter.getCurrentInstance().setSessionAttribute(CONVERSATION_MANAGER_KEY, conversationManager);
124         }
125 
126         return conversationManager;
127     }
128 
129     /**
130      * Get the current conversationContextId.
131      * <p>
132      * If there is no current conversationContext, then null is returned.
133      */
134     private Long findConversationContextId()
135     {
136         FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
137         
138         // Has it been extracted from the req params and cached as a req attr?
139         Long conversationContextId = (Long)fa.getRequestAttribute(CONVERSATION_CONTEXT_REQ);
140         if (conversationContextId == null)
141         {
142             if (fa.containsRequestParameterAttribute(CONVERSATION_CONTEXT_PARAM))
143             {
144                 String urlConversationContextId = fa.getRequestParameterAttribute(
145                         CONVERSATION_CONTEXT_PARAM).toString();
146                 conversationContextId = new Long(
147                         Long.parseLong(urlConversationContextId, Character.MAX_RADIX));
148             }
149         }
150         return conversationContextId;
151     }
152     
153     /**
154      * Get the current, or create a new unique conversationContextId.
155      * <p>
156      * The current conversationContextId will be retrieved from the request
157      * parameters. If no such parameter is present then a new id will be
158      * allocated <i>and configured as the current conversation id</i>.
159      * <p>
160      * In either case the result will be stored within the request for
161      * faster lookup.
162      * <p>
163      * Note that there is no security flaw regarding injection of fake
164      * context ids; the id must match one already in the session and there
165      * is no security problem with two windows in the same session exchanging
166      * ids.
167      * <p>
168      * This method <i>never</i> returns null.
169      */
170     private Long getOrCreateConversationContextId()
171     {
172         Long conversationContextId = findConversationContextId();
173         if (conversationContextId == null)
174         {
175             conversationContextId = createNextConversationContextId();
176             FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
177             fa.setRequestAttribute(CONVERSATION_CONTEXT_REQ, conversationContextId);
178         }
179 
180         return conversationContextId;
181     }
182 
183     /**
184      * Get the current, or create a new unique conversationContextId.
185      * <p>
186      * This method is deprecated because, unlike all the other get methods, it
187      * actually creates the value if it does not exist. Other get methods (except
188      * getInstance) return null if the data does not exist. In addition, this
189      * method is not really useful to external code and probably should never
190      * have been exposed as a public API in the first place; external code should
191      * never need to force the creation of a ConversationContext.
192      * <p>
193      * For internal use within this class, use either findConversationContextId()
194      * or getOrCreateConversationContextId().
195      * <p>
196      * To just obtain the current ConversationContext <i>if it exists</i>, see
197      * method getCurrentConversationContext().
198      * 
199      * @deprecated This method should not be needed by external classes, and
200      * was inconsistent with other methods on this class.
201      */
202     public Long getConversationContextId()
203     {
204         return getOrCreateConversationContextId();
205     }
206 
207     /**
208      * Allocate a new Long value for use as a conversation context id.
209      * <p>
210      * The returned value must not match any conversation context id already in
211      * use within this ConversationManager instance (which is scoped to the 
212      * current http session).
213      */
214     private Long createNextConversationContextId()
215     {
216         Long conversationContextId;
217         synchronized(this)
218         {
219             conversationContextId = new Long(nextConversationContextId);
220             nextConversationContextId++;
221         }
222         return conversationContextId;
223     }
224 
225     /**
226      * Get the conversation context for the given id.
227      * <p>
228      * Null is returned if there is no ConversationContext with the specified id.
229      * <p>
230      * Param conversationContextId must not be null.
231      */
232     protected ConversationContext getConversationContext(Long conversationContextId)
233     {
234         synchronized (this)
235         {
236             return (ConversationContext) conversationContexts.get(conversationContextId);
237         }
238     }
239 
240     /**
241      * Get the conversation context for the given id.
242      * <p>
243      * If there is no such conversation context a new one will be created.
244      * The new conversation context will be a "top-level" context (ie has no parent).
245      * <p>
246      * The new conversation context will <i>not</i> be the current conversation context,
247      * unless the id passed in was already configured as the current conversation context id.
248      */
249     protected ConversationContext getOrCreateConversationContext(Long conversationContextId)
250     {
251         synchronized (this)
252         {
253             ConversationContext conversationContext = (ConversationContext) conversationContexts.get(
254                     conversationContextId);
255             if (conversationContext == null)
256             {
257                 conversationContext = new ConversationContext(null, conversationContextId.longValue());
258                 conversationContexts.put(conversationContextId, conversationContext);
259 
260                 // TODO: add the "user" name here, otherwise this debugging is not very useful
261                 // except when testing a webapp with only one user.
262                 log.debug("Created context " + conversationContextId);
263             }
264             return conversationContext;
265         }
266     }
267 
268     /**
269      * Make the specific context the current context for the current HTTP session.
270      * <p>
271      * Methods like getCurrentConversationContext will then return the specified
272      * context object.
273      * 
274      * @since 1.2
275      */
276     public void activateConversationContext(ConversationContext ctx)
277     {
278         FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
279         fa.setRequestAttribute(CONVERSATION_CONTEXT_REQ, ctx.getIdAsLong());
280     }
281 
282     /**
283      * Ends all conversations within the current context; the context itself will remain active.
284      */
285     public void clearCurrentConversationContext()
286     {
287         Long conversationContextId = findConversationContextId();
288         if (conversationContextId != null)
289         {
290             ConversationContext conversationContext = getConversationContext(conversationContextId);
291             if (conversationContext != null)
292             {
293                 conversationContext.clear();
294             }
295         }
296     }
297 
298     /**
299      * Destroy the given conversation context.
300      * <p>
301      * Note: it is assumed that the context is already been destroyed
302      */
303     protected void removeConversationContext(Long conversationContextId)
304     {
305         synchronized (this)
306         {
307             conversationContexts.remove(conversationContextId);
308         }
309     }
310 
311     /**
312      * Start a conversation.
313      *
314      * @see ConversationContext#startConversation(String, ConversationFactory)
315      */
316     public Conversation startConversation(String name, ConversationFactory factory)
317     {
318         ConversationContext conversationContext = getOrCreateCurrentConversationContext();
319         return conversationContext.startConversation(name, factory);
320     }
321 
322     /**
323      * Remove a conversation
324      *
325      * Note: It is assumed that the conversation has already been invalidated
326      *
327      * @see ConversationContext#removeConversation(String)
328      */
329     protected void removeConversation(String name)
330     {
331         Long conversationContextId = findConversationContextId();
332         if (conversationContextId != null)
333         {
334             ConversationContext conversationContext = getConversationContext(conversationContextId);
335             if (conversationContext != null)
336             {
337                 conversationContext.removeConversation(name);
338             }
339         }
340     }
341 
342     /**
343      * Get the conversation with the given name
344      *
345      * @return null if no conversation context is active or if the conversation did not exist.
346      */
347     public Conversation getConversation(String name)
348     {
349         ConversationContext conversationContext = getCurrentConversationContext();
350         if (conversationContext == null)
351         {
352             return null;
353         }
354         return conversationContext.getConversation(name);
355     }
356 
357     /**
358      * check if the given conversation is active
359      */
360     public boolean hasConversation(String name)
361     {
362         ConversationContext conversationContext = getCurrentConversationContext();
363         if (conversationContext == null)
364         {
365             return false;
366         }
367         return conversationContext.hasConversation(name);
368     }
369 
370     /**
371      * Returns an iterator over all the Conversation objects in the current conversation
372      * context. Never returns null, even if no conversation context exists.
373      */
374     public Iterator iterateConversations()
375     {
376         ConversationContext conversationContext = getCurrentConversationContext();
377         if (conversationContext == null)
378         {
379             return EMPTY_ITERATOR;
380         }
381 
382         return conversationContext.iterateConversations();
383     }
384 
385     /**
386      * Get the current conversation context.
387      * <p>
388      * In a simple Orchestra application this will always be a root conversation context.
389      * When using a dialog/page-flow environment the context that is returned might have
390      * a parent context.
391      * <p>
392      * Null is returned if there is no current conversationContext.
393      */
394     public ConversationContext getCurrentConversationContext()
395     {
396         Long ccid = findConversationContextId();
397         if (ccid == null)
398         {
399             return null;
400         }
401         else
402         {
403             return getConversationContext(ccid);
404         }
405     }
406 
407     /**
408      * Return the current ConversationContext for the current http session;
409      * if none yet exists then a ConversationContext is created and configured
410      * as the current context.
411      * <p>
412      * This is currently package-scoped because it is not clear that code
413      * outside orchestra can have any use for this method. The only user
414      * outside of this class is ConversationRequestParameterProvider.
415      * 
416      * @since 1.2
417      */
418     ConversationContext getOrCreateCurrentConversationContext()
419     {
420         Long ccid = getOrCreateConversationContextId();
421         return getOrCreateConversationContext(ccid);
422     }
423 
424     /**
425      * Return true if there is a conversation context associated with the
426      * current request.
427      */
428     public boolean hasConversationContext()
429     {
430         return getCurrentConversationContext() == null;
431     }
432 
433     /**
434      * Get the current root conversation context (aka the window conversation context).
435      * <p>
436      * Null is returned if it does not exist.
437      * 
438      * @since 1.2
439      */
440     public ConversationContext getCurrentRootConversationContext()
441     {
442         Long ccid = findConversationContextId();
443         if (ccid == null)
444         {
445             return null;
446         }
447 
448         synchronized (this)
449         {
450             ConversationContext conversationContext = getConversationContext(ccid);
451             if (conversationContext == null)
452             {
453                 return null;
454             }
455             else
456             {
457                 return conversationContext.getRoot();
458             }
459         }
460     }
461 
462     /**
463      * Get the Messager used to inform the user about anomalies.
464      * <p>
465      * What instance is returned is controlled by the FrameworkAdapter. See
466      * {@link org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter} for details.
467      */
468     public ConversationMessager getMessager()
469     {
470         return FrameworkAdapter.getCurrentInstance().getConversationMessager();
471     }
472 
473     /**
474      * Check the timeout for each conversation context, and all conversations
475      * within those contexts.
476      * <p>
477      * If any conversation has not been accessed within its timeout period
478      * then clear the context.
479      * <p>
480      * Invoke the checkTimeout method on each context so that any conversation
481      * that has not been accessed within its timeout is invalidated.
482      */
483     protected void checkTimeouts()
484     {
485         Map.Entry[] contexts;
486         synchronized (this)
487         {
488             contexts = new Map.Entry[conversationContexts.size()];
489             conversationContexts.entrySet().toArray(contexts);
490         }
491 
492         long checkTime = System.currentTimeMillis();
493 
494         for (int i = 0; i<contexts.length; i++)
495         {
496             Map.Entry context = contexts[i];
497 
498             Long conversationContextId = (Long) context.getKey();
499             ConversationContext conversationContext = (ConversationContext) context.getValue();
500             conversationContext.checkConversationTimeout();
501 
502             if (conversationContext.getTimeout() > -1 &&
503                 (conversationContext.getLastAccess() +
504                 conversationContext.getTimeout()) < checkTime)
505             {
506                 if (log.isDebugEnabled())
507                 {
508                     log.debug("end conversation context due to timeout: " + conversationContext.getId());
509                 }
510 
511                 conversationContext.clear();
512                 synchronized (this)
513                 {
514                     conversationContexts.remove(conversationContextId);
515                 }
516             }
517         }
518     }
519 
520     private void writeObject(java.io.ObjectOutputStream out) throws IOException
521     {
522         // the conversation manager is not (yet) serializable, we just implement it
523         // to make it work with distributed sessions
524     }
525 
526     private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
527     {
528         // nothing written, so nothing to read
529     }
530 
531     private Object readResolve() throws ObjectStreamException
532     {
533         // do not return a real object, that way on first request a new conversation manager will be created
534         return null;
535     }
536 }