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