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 java.io.IOException;
23  import java.io.ObjectStreamException;
24  import java.io.Serializable;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.Iterator;
28  import java.util.Map;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.apache.myfaces.orchestra.FactoryFinder;
33  import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
34  import org.apache.myfaces.orchestra.lib.OrchestraException;
35  import org.apache.myfaces.orchestra.requestParameterProvider.RequestParameterProviderManager;
36  
37  /**
38   * Deals with the various conversation contexts in the current session.
39   * <p>
40   * There is expected to be one instance of this class per http-session, managing all of the
41   * data associated with all browser windows that use that http-session.
42   * <p>
43   * One particular task of this class is to return "the current" ConversationContext object for
44   * the current http request (from the set of ConversationContext objects that this manager
45   * object holds). The request url is presumed to include a query-parameter that specifies the
46   * id of the appropriate ConversationContext object to be used. If no such query-parameter is
47   * present, then a new ConversationContext object will automatically be created.
48   * <p>
49   * At the current time, this object does not serialize well. Any attempt to serialize
50   * this object (including any serialization of the user session) will just cause it
51   * to be discarded.
52   * <p>
53   * TODO: fix serialization issues.
54   */
55  public class ConversationManager implements Serializable
56  {
57      private static final long serialVersionUID = 1L;
58  
59      final static String CONVERSATION_CONTEXT_PARAM = "conversationContext";
60  
61      private final static String CONVERSATION_MANAGER_KEY = "org.apache.myfaces.ConversationManager";
62      private final static String CONVERSATION_CONTEXT_REQ = "org.apache.myfaces.ConversationManager.conversationContext";
63  
64      private static final Iterator EMPTY_ITERATOR = Collections.EMPTY_LIST.iterator();
65  
66      // See method readResolve
67      private static final Object DUMMY = new Integer(-1);
68  
69      private final Log log = LogFactory.getLog(ConversationManager.class);
70  
71      /**
72       * Used to generate a unique id for each "window" that a user has open
73       * on the same webapp within the same HttpSession. Note that this is a
74       * property of an object stored in the session, so will correctly
75       * migrate from machine to machine along with a distributed HttpSession.
76       *
77       */
78      private long nextConversationContextId = 1;
79  
80      // This member must always be accessed with a lock held on the parent ConverstationManager instance;
81      // a HashMap is not thread-safe and this class must be thread-safe.
82      private final Map conversationContexts = new HashMap();
83  
84      protected ConversationManager()
85      {
86      }
87  
88      /**
89       * Get the conversation manager for the current http session.
90       * <p>
91       * If none exists, then a new instance is allocated and stored in the current http session.
92       * Null is never returned.
93       * <p>
94       * Throws IllegalStateException if the Orchestra FrameworkAdapter has not been correctly
95       * configured.
96       */
97      public static ConversationManager getInstance()
98      {
99          return getInstance(true);
100     }
101 
102     /**
103      * Get the conversation manager for the current http session.
104      * <p>
105      * When create is true, an instance is always returned; one is created if none currently exists
106      * for the current user session.
107      * <p>
108      * When create is false, null is returned if no instance yet exists for the current user session.
109      */
110     public static ConversationManager getInstance(boolean create)
111     {
112         FrameworkAdapter frameworkAdapter = FrameworkAdapter.getCurrentInstance();
113         if (frameworkAdapter == null)
114         {
115             if (!create)
116             {
117                 // if we don't have to create a conversation manager, then it doesn't
118                 // matter if there is no FrameworkAdapter available.
119                 return null;
120             }
121             else
122             {
123                 throw new IllegalStateException("FrameworkAdapter not found");
124             }
125         }
126 
127         Object cmObj = frameworkAdapter.getSessionAttribute(CONVERSATION_MANAGER_KEY);
128         // hack: see method readResolve
129         if (cmObj == DUMMY)
130         {
131             Log log = LogFactory.getLog(ConversationManager.class);
132             log.debug("Method getInstance found dummy ConversationManager object");
133             cmObj = null;
134         }
135 
136 
137         ConversationManager conversationManager = (ConversationManager) cmObj;
138 
139         if (conversationManager == null && create)
140         {
141             Log log = LogFactory.getLog(ConversationManager.class);
142             log.debug("Register ConversationRequestParameterProvider");
143             
144             conversationManager = FactoryFinder.getConversationManagerFactory().createConversationManager();
145 
146             // initialize environmental systems
147             RequestParameterProviderManager.getInstance().register(new ConversationRequestParameterProvider());
148 
149             // set mark
150             FrameworkAdapter.getCurrentInstance().setSessionAttribute(CONVERSATION_MANAGER_KEY, conversationManager);
151         }
152 
153         return conversationManager;
154     }
155 
156     /**
157      * Get the current conversationContextId.
158      * <p>
159      * If there is no current conversationContext, then null is returned.
160      */
161     private Long findConversationContextId()
162     {
163         FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
164         
165         // Has it been extracted from the req params and cached as a req attr?
166         Long conversationContextId = (Long)fa.getRequestAttribute(CONVERSATION_CONTEXT_REQ);
167         if (conversationContextId == null)
168         {
169             if (fa.containsRequestParameterAttribute(CONVERSATION_CONTEXT_PARAM))
170             {
171                 String urlConversationContextId = fa.getRequestParameterAttribute(
172                         CONVERSATION_CONTEXT_PARAM).toString();
173                 conversationContextId = new Long(
174                         Long.parseLong(urlConversationContextId, Character.MAX_RADIX));
175             }
176         }
177         return conversationContextId;
178     }
179     
180     /**
181      * Get the current, or create a new unique conversationContextId.
182      * <p>
183      * The current conversationContextId will be retrieved from the request
184      * parameters. If no such parameter is present then a new id will be
185      * allocated <i>and configured as the current conversation id</i>.
186      * <p>
187      * In either case the result will be stored within the request for
188      * faster lookup.
189      * <p>
190      * Note that there is no security flaw regarding injection of fake
191      * context ids; the id must match one already in the session and there
192      * is no security problem with two windows in the same session exchanging
193      * ids.
194      * <p>
195      * This method <i>never</i> returns null.
196      */
197     private Long getOrCreateConversationContextId()
198     {
199         Long conversationContextId = findConversationContextId();
200         if (conversationContextId == null)
201         {
202             conversationContextId = createNextConversationContextId();
203             FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
204             fa.setRequestAttribute(CONVERSATION_CONTEXT_REQ, conversationContextId);
205         }
206 
207         return conversationContextId;
208     }
209 
210     /**
211      * Get the current, or create a new unique conversationContextId.
212      * <p>
213      * This method is deprecated because, unlike all the other get methods, it
214      * actually creates the value if it does not exist. Other get methods (except
215      * getInstance) return null if the data does not exist. In addition, this
216      * method is not really useful to external code and probably should never
217      * have been exposed as a public API in the first place; external code should
218      * never need to force the creation of a ConversationContext.
219      * <p>
220      * For internal use within this class, use either findConversationContextId()
221      * or getOrCreateConversationContextId().
222      * <p>
223      * To just obtain the current ConversationContext <i>if it exists</i>, see
224      * method getCurrentConversationContext().
225      * 
226      * @deprecated This method should not be needed by external classes, and
227      * was inconsistent with other methods on this class.
228      */
229     public Long getConversationContextId()
230     {
231         return getOrCreateConversationContextId();
232     }
233 
234     /**
235      * Allocate a new Long value for use as a conversation context id.
236      * <p>
237      * The returned value must not match any conversation context id already in
238      * use within this ConversationManager instance (which is scoped to the 
239      * current http session).
240      */
241     private Long createNextConversationContextId()
242     {
243         Long conversationContextId;
244         synchronized(this)
245         {
246             conversationContextId = new Long(nextConversationContextId);
247             nextConversationContextId++;
248         }
249         return conversationContextId;
250     }
251 
252     /**
253      * Get the conversation context for the given id.
254      * <p>
255      * Null is returned if there is no ConversationContext with the specified id.
256      * <p>
257      * Param conversationContextId must not be null.
258      * <p>
259      * Public since version 1.3.
260      */
261     public ConversationContext getConversationContext(Long conversationContextId)
262     {
263         synchronized (this)
264         {
265             return (ConversationContext) conversationContexts.get(conversationContextId);
266         }
267     }
268 
269     /**
270      * Get the conversation context for the given id.
271      * <p>
272      * If there is no such conversation context a new one will be created.
273      * The new conversation context will be a "top-level" context (ie has no parent).
274      * <p>
275      * The new conversation context will <i>not</i> be the current conversation context,
276      * unless the id passed in was already configured as the current conversation context id.
277      */
278     protected ConversationContext getOrCreateConversationContext(Long conversationContextId)
279     {
280         synchronized (this)
281         {
282             ConversationContext conversationContext = (ConversationContext) conversationContexts.get(
283                     conversationContextId);
284             if (conversationContext == null)
285             {
286                 ConversationContextFactory factory = FactoryFinder.getConversationContextFactory();
287                 conversationContext = factory.createConversationContext(null, conversationContextId.longValue());
288                 conversationContexts.put(conversationContextId, conversationContext);
289 
290                 // TODO: add the "user" name here, otherwise this debugging is not very useful
291                 // except when testing a webapp with only one user.
292                 log.debug("Created context " + conversationContextId);
293             }
294             return conversationContext;
295         }
296     }
297 
298     /**
299      * This will create a new conversation context using the specified context as
300      * its parent. 
301      * <p>
302      * The returned context is not selected as the "current" one; see activateConversationContext.
303      * 
304      * @since 1.3
305      */
306     public ConversationContext createConversationContext(ConversationContext parent)
307     {
308         Long ctxId = createNextConversationContextId();
309         ConversationContextFactory factory = FactoryFinder.getConversationContextFactory();
310         ConversationContext ctx = factory.createConversationContext(parent, ctxId.longValue());
311 
312         synchronized(this)
313         {
314             conversationContexts.put(ctxId, ctx);
315         }
316         
317         return ctx;
318     }
319 
320     /**
321      * Make the specific context the current context for the current HTTP session.
322      * <p>
323      * Methods like getCurrentConversationContext will then return the specified
324      * context object.
325      * 
326      * @since 1.2
327      */
328     public void activateConversationContext(ConversationContext ctx)
329     {
330         FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
331         fa.setRequestAttribute(CONVERSATION_CONTEXT_REQ, ctx.getIdAsLong());
332     }
333 
334     /**
335      * Ends all conversations within the current context; the context itself will remain active.
336      */
337     public void clearCurrentConversationContext()
338     {
339         Long conversationContextId = findConversationContextId();
340         if (conversationContextId != null)
341         {
342             ConversationContext conversationContext = getConversationContext(conversationContextId);
343             if (conversationContext != null)
344             {
345                 conversationContext.invalidate();
346             }
347         }
348     }
349 
350     /**
351      * Removes the specified contextId from the set of known contexts,
352      * and deletes every conversation in it.
353      * <p>
354      * Objects in the conversation which implement ConversationAware
355      * will have callbacks invoked.
356      * <p>
357      * The conversation being removed must not be the currently active
358      * context. If it is, then method activateConversationContext should
359      * first be called on some other instance (perhaps the parent of the
360      * one being removed) before this method is called.
361      * 
362      * @since 1.3
363      */
364     public void removeAndInvalidateConversationContext(ConversationContext context)
365     {
366         if (context.hasChildren())
367         {
368             throw new OrchestraException("Cannot remove context with children");
369         }
370 
371         if (context.getIdAsLong().equals(findConversationContextId()))
372         {
373             throw new OrchestraException("Cannot remove current context");
374         }
375 
376         synchronized(conversationContexts)
377         {
378             conversationContexts.remove(context.getIdAsLong());
379         }
380 
381         ConversationContext parent = context.getParent();
382         if (parent != null)
383         {
384             parent.removeChild(context);
385         }
386 
387         context.invalidate();
388         
389         // TODO: add the deleted context ids to a list stored in the session,
390         // and redirect to an error page if any future request specifies this id.
391         // This catches things like going "back" into a flow that has ended, or
392         // navigating with the parent page of a popup flow (which kills the popup
393         // flow context) then trying to use the popup page.
394         //
395         // We cannot simply report an error for every case where an invalid id is
396         // used, because bookmarks will have ids in them; when the bookmark is used
397         // after the session has died we still want the bookmark url to work. Possibly
398         // we should allow GET with a bad id, but always fail a POST with one?
399     }
400 
401     /**
402      * Removes the specified contextId from the set of known contexts.
403      * <p>
404      * It does nothing else. Maybe it should be called "detachConversationContext"
405      * or similar.
406      * 
407      * @deprecated This method is not actually used by anything.
408      */
409     protected void removeConversationContext(Long conversationContextId)
410     {
411         synchronized (this)
412         {
413             conversationContexts.remove(conversationContextId);
414         }
415     }
416 
417     /**
418      * Start a conversation.
419      *
420      * @see ConversationContext#startConversation(String, ConversationFactory)
421      */
422     public Conversation startConversation(String name, ConversationFactory factory)
423     {
424         ConversationContext conversationContext = getOrCreateCurrentConversationContext();
425         return conversationContext.startConversation(name, factory);
426     }
427 
428     /**
429      * Remove a conversation
430      *
431      * Note: It is assumed that the conversation has already been invalidated
432      *
433      * @see ConversationContext#removeConversation(String)
434      */
435     protected void removeConversation(String name)
436     {
437         Long conversationContextId = findConversationContextId();
438         if (conversationContextId != null)
439         {
440             ConversationContext conversationContext = getConversationContext(conversationContextId);
441             if (conversationContext != null)
442             {
443                 conversationContext.removeConversation(name);
444             }
445         }
446     }
447 
448     /**
449      * Get the conversation with the given name
450      *
451      * @return null if no conversation context is active or if the conversation did not exist.
452      */
453     public Conversation getConversation(String name)
454     {
455         ConversationContext conversationContext = getCurrentConversationContext();
456         if (conversationContext == null)
457         {
458             return null;
459         }
460         return conversationContext.getConversation(name);
461     }
462 
463     /**
464      * check if the given conversation is active
465      */
466     public boolean hasConversation(String name)
467     {
468         ConversationContext conversationContext = getCurrentConversationContext();
469         if (conversationContext == null)
470         {
471             return false;
472         }
473         return conversationContext.hasConversation(name);
474     }
475 
476     /**
477      * Returns an iterator over all the Conversation objects in the current conversation
478      * context. Never returns null, even if no conversation context exists.
479      */
480     public Iterator iterateConversations()
481     {
482         ConversationContext conversationContext = getCurrentConversationContext();
483         if (conversationContext == null)
484         {
485             return EMPTY_ITERATOR;
486         }
487 
488         return conversationContext.iterateConversations();
489     }
490 
491     /**
492      * Get the current conversation context.
493      * <p>
494      * In a simple Orchestra application this will always be a root conversation context.
495      * When using a dialog/page-flow environment the context that is returned might have
496      * a parent context.
497      * <p>
498      * Null is returned if there is no current conversationContext.
499      */
500     public ConversationContext getCurrentConversationContext()
501     {
502         Long ccid = findConversationContextId();
503         if (ccid == null)
504         {
505             return null;
506         }
507         else
508         {
509             ConversationContext ctx = getConversationContext(ccid);
510             if (ctx == null)
511             {
512                 // Someone has perhaps used the back button to go back into a context
513                 // that has already ended. This simply will not work, so we should
514                 // throw an exception here.
515                 //
516                 // Or somebody might have just activated a bookmark. Unfortunately,
517                 // when someone bookmarks a page within an Orchestra app, the bookmark
518                 // will capture the contextId too.
519                 //
520                 // There is unfortunately no obvious way to tell these two actions apart.
521                 // So we cannot report an error here; instead, just return a null context
522                 // so that a new instance gets created - and hope that the page itself
523                 // detects the problem and reports an error if it needs conversation state
524                 // that does not exist.
525                 //
526                 // What we should do here *at least* is bump the nextConversationId value
527                 // to be greater than this value, so that we don't later try to allocate a
528                 // second conversation with the same id. Yes, evil users could pass a very
529                 // high value here and cause wraparound but that is really not a problem as
530                 // they can only screw themselves up.
531                 log.warn("ConversationContextId specified but context does not exist");
532                 synchronized(this)
533                 {
534                     if (nextConversationContextId <= ccid.longValue())
535                     {
536                         nextConversationContextId = ccid.longValue() + 1;
537                     }
538                 }
539                 return null;
540             }
541             return ctx;
542         }
543     }
544 
545     /**
546      * Return the current ConversationContext for the current http session;
547      * if none yet exists then a ConversationContext is created and configured
548      * as the current context.
549      * <p>
550      * This is currently package-scoped because it is not clear that code
551      * outside orchestra can have any use for this method. The only user
552      * outside of this class is ConversationRequestParameterProvider.
553      * 
554      * @since 1.2
555      */
556     ConversationContext getOrCreateCurrentConversationContext()
557     {
558         Long ccid = getOrCreateConversationContextId();
559         return getOrCreateConversationContext(ccid);
560     }
561 
562     /**
563      * Return true if there is a conversation context associated with the
564      * current request.
565      */
566     public boolean hasConversationContext()
567     {
568         return getCurrentConversationContext() == null;
569     }
570 
571     /**
572      * Get the current root conversation context (aka the window conversation context).
573      * <p>
574      * Null is returned if it does not exist.
575      * 
576      * @since 1.2
577      */
578     public ConversationContext getCurrentRootConversationContext()
579     {
580         Long ccid = findConversationContextId();
581         if (ccid == null)
582         {
583             return null;
584         }
585 
586         synchronized (this)
587         {
588             ConversationContext conversationContext = getConversationContext(ccid);
589             if (conversationContext == null)
590             {
591                 return null;
592             }
593             else
594             {
595                 return conversationContext.getRoot();
596             }
597         }
598     }
599 
600     /**
601      * Get the Messager used to inform the user about anomalies.
602      * <p>
603      * What instance is returned is controlled by the FrameworkAdapter. See
604      * {@link org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter} for details.
605      */
606     public ConversationMessager getMessager()
607     {
608         return FrameworkAdapter.getCurrentInstance().getConversationMessager();
609     }
610 
611     /**
612      * Check the timeout for each conversation context, and all conversations
613      * within those contexts.
614      * <p>
615      * If any conversation has not been accessed within its timeout period
616      * then clear the context.
617      * <p>
618      * Invoke the checkTimeout method on each context so that any conversation
619      * that has not been accessed within its timeout is invalidated.
620      */
621     protected void checkTimeouts()
622     {
623         Map.Entry[] contexts;
624         synchronized (this)
625         {
626             contexts = new Map.Entry[conversationContexts.size()];
627             conversationContexts.entrySet().toArray(contexts);
628         }
629 
630         long checkTime = System.currentTimeMillis();
631 
632         for (int i = 0; i<contexts.length; i++)
633         {
634             Map.Entry context = contexts[i];
635 
636             ConversationContext conversationContext = (ConversationContext) context.getValue();
637             if (conversationContext.hasChildren())
638             {
639                 // Never time out contexts that have children. Let the children time out first...
640                 continue;
641             }
642 
643             conversationContext.checkConversationTimeout();
644 
645             if (conversationContext.getTimeout() > -1 &&
646                 (conversationContext.getLastAccess() +
647                 conversationContext.getTimeout()) < checkTime)
648             {
649                 if (log.isDebugEnabled())
650                 {
651                     log.debug("end conversation context due to timeout: " + conversationContext.getId());
652                 }
653 
654                 removeAndInvalidateConversationContext(conversationContext);
655             }
656         }
657     }
658 
659     /**
660      * @since 1.4
661      */
662     public void removeAndInvalidateAllConversationContexts()
663     {
664         ConversationContext[] contexts;
665         synchronized (this)
666         {
667             contexts = new ConversationContext[conversationContexts.size()];
668             conversationContexts.values().toArray(contexts);
669         }
670 
671         for (int i = 0; i<contexts.length; i++)
672         {
673             ConversationContext context = contexts[i];
674             removeAndInvalidateConversationContextAndChildren(context);
675         }
676     }
677 
678     private void removeAndInvalidateConversationContextAndChildren(ConversationContext conversationContext)
679     {
680         while (conversationContext.hasChildren())
681         {
682             // Get first child
683             ConversationContext child = (ConversationContext) conversationContext.getChildren().iterator().next();
684 
685             // This call removes child from conversationContext.children
686             removeAndInvalidateConversationContextAndChildren(child);
687         }
688 
689         if (log.isDebugEnabled())
690         {
691             log.debug("end conversation context: " + conversationContext.getId());
692         }
693 
694         removeAndInvalidateConversationContext(conversationContext);
695     }
696 
697     private void writeObject(java.io.ObjectOutputStream out) throws IOException
698     {
699         // the conversation manager is not (yet) serializable, we just implement it
700         // to make it work with distributed sessions
701     }
702 
703     private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
704     {
705         // nothing written, so nothing to read
706     }
707 
708     private Object readResolve() throws ObjectStreamException
709     {
710         // Note: Returning null here is not a good idea (for Tomcat 6.0.16 at least). Null objects are
711         // not permitted within an HttpSession; calling HttpSession.setAttribute(name, null) is defined as
712         // removing the attribute. So returning null here when deserializing an object from the session
713         // can cause problems.
714         //
715         // Note that nothing should have a reference to the ConversationManager *except* the entry
716         // in the http session; all other code should look it up "on demand" via the getInstance
717         // method rather than storing a reference to it. So we can do pretty much anything we like
718         // here as long as the getInstance() method works correctly later. Thus:
719         //  * returning null here is one option (getInstance just creates the item later) - except
720         //    that tomcat doesn't like it.
721         // * creating a new object instance that getInstance will later simply find and return will
722         //   work - except that the actual type to create can be overridden via the dependency-injection
723         //   config, and the FrameworkAdapter class that gives us access to that info is not available
724         //   at the current time.
725         //
726         // To solve this, we use a hack: a special DUMMY object is returned (and therefore will be inserted
727         // into the HTTP session under the ConversationManager key). The getInstance method then checks
728         // for this dummy object, and treats it like NULL. Conveniently, it appears that the serialization
729         // mechanism doesn't care if readResolve returns an object that is not a subclass of the one that
730         // is being deserialized, so here we can return any old object (eg an Integer).
731         //
732         // An alternative would be to just remove the ConversationManager object from the http session
733         // on passivate, so that this readResolve method is never called. However hopefully at some
734         // future time we *will* get serialization for this class working nicely and then will need
735         // to discard these serialization hacks; it is easier to do that when the hacks are all in
736         // the same class.
737 
738         Log log = LogFactory.getLog(ConversationManager.class);
739         log.debug("readResolve returning dummy ConversationManager object");
740         return DUMMY;
741     }
742 }