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 }