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