/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.myfaces.orchestra.conversation;
import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.myfaces.orchestra.FactoryFinder;
import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
import org.apache.myfaces.orchestra.lib.OrchestraException;
import org.apache.myfaces.orchestra.requestParameterProvider.RequestParameterProviderManager;
/**
* Deals with the various conversation contexts in the current session.
* <p>
* There is expected to be one instance of this class per http-session, managing all of the
* data associated with all browser windows that use that http-session.
* <p>
* One particular task of this class is to return "the current" ConversationContext object for
* the current http request (from the set of ConversationContext objects that this manager
* object holds). The request url is presumed to include a query-parameter that specifies the
* id of the appropriate ConversationContext object to be used. If no such query-parameter is
* present, then a new ConversationContext object will automatically be created.
* <p>
* At the current time, this object does not serialize well. Any attempt to serialize
* this object (including any serialization of the user session) will just cause it
* to be discarded.
* <p>
* TODO: fix serialization issues.
*/
public class ConversationManager implements Serializable
{
private static final long serialVersionUID = 1L;
final static String CONVERSATION_CONTEXT_PARAM = "conversationContext";
private final static String CONVERSATION_MANAGER_KEY = "org.apache.myfaces.ConversationManager";
private final static String CONVERSATION_CONTEXT_REQ = "org.apache.myfaces.ConversationManager.conversationContext";
private static final Iterator EMPTY_ITERATOR = Collections.EMPTY_LIST.iterator();
// See method readResolve
private static final Object DUMMY = new Integer(-1);
private final Log log = LogFactory.getLog(ConversationManager.class);
/**
* Used to generate a unique id for each "window" that a user has open
* on the same webapp within the same HttpSession. Note that this is a
* property of an object stored in the session, so will correctly
* migrate from machine to machine along with a distributed HttpSession.
*
*/
private long nextConversationContextId = 1;
// This member must always be accessed with a lock held on the parent ConverstationManager instance;
// a HashMap is not thread-safe and this class must be thread-safe.
private final Map conversationContexts = new HashMap();
protected ConversationManager()
{
}
/**
* Get the conversation manager for the current http session.
* <p>
* If none exists, then a new instance is allocated and stored in the current http session.
* Null is never returned.
* <p>
* Throws IllegalStateException if the Orchestra FrameworkAdapter has not been correctly
* configured.
*/
public static ConversationManager getInstance()
{
return getInstance(true);
}
/**
* Get the conversation manager for the current http session.
* <p>
* When create is true, an instance is always returned; one is created if none currently exists
* for the current user session.
* <p>
* When create is false, null is returned if no instance yet exists for the current user session.
*/
public static ConversationManager getInstance(boolean create)
{
FrameworkAdapter frameworkAdapter = FrameworkAdapter.getCurrentInstance();
if (frameworkAdapter == null)
{
if (!create)
{
// if we don't have to create a conversation manager, then it doesn't
// matter if there is no FrameworkAdapter available.
return null;
}
else
{
throw new IllegalStateException("FrameworkAdapter not found");
}
}
Object cmObj = frameworkAdapter.getSessionAttribute(CONVERSATION_MANAGER_KEY);
// hack: see method readResolve
if (cmObj == DUMMY)
{
Log log = LogFactory.getLog(ConversationManager.class);
log.debug("Method getInstance found dummy ConversationManager object");
cmObj = null;
}
ConversationManager conversationManager = (ConversationManager) cmObj;
if (conversationManager == null && create)
{
Log log = LogFactory.getLog(ConversationManager.class);
log.debug("Register ConversationRequestParameterProvider");
conversationManager = FactoryFinder.getConversationManagerFactory().createConversationManager();
// initialize environmental systems
RequestParameterProviderManager.getInstance().register(new ConversationRequestParameterProvider());
// set mark
FrameworkAdapter.getCurrentInstance().setSessionAttribute(CONVERSATION_MANAGER_KEY, conversationManager);
}
return conversationManager;
}
/**
* Get the current conversationContextId.
* <p>
* If there is no current conversationContext, then null is returned.
*/
private Long findConversationContextId()
{
FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
// Has it been extracted from the req params and cached as a req attr?
Long conversationContextId = (Long)fa.getRequestAttribute(CONVERSATION_CONTEXT_REQ);
if (conversationContextId == null)
{
if (fa.containsRequestParameterAttribute(CONVERSATION_CONTEXT_PARAM))
{
String urlConversationContextId = fa.getRequestParameterAttribute(
CONVERSATION_CONTEXT_PARAM).toString();
conversationContextId = new Long(
Long.parseLong(urlConversationContextId, Character.MAX_RADIX));
}
}
return conversationContextId;
}
/**
* Get the current, or create a new unique conversationContextId.
* <p>
* The current conversationContextId will be retrieved from the request
* parameters. If no such parameter is present then a new id will be
* allocated <i>and configured as the current conversation id</i>.
* <p>
* In either case the result will be stored within the request for
* faster lookup.
* <p>
* Note that there is no security flaw regarding injection of fake
* context ids; the id must match one already in the session and there
* is no security problem with two windows in the same session exchanging
* ids.
* <p>
* This method <i>never</i> returns null.
*/
private Long getOrCreateConversationContextId()
{
Long conversationContextId = findConversationContextId();
if (conversationContextId == null)
{
conversationContextId = createNextConversationContextId();
FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
fa.setRequestAttribute(CONVERSATION_CONTEXT_REQ, conversationContextId);
}
return conversationContextId;
}
/**
* Get the current, or create a new unique conversationContextId.
* <p>
* This method is deprecated because, unlike all the other get methods, it
* actually creates the value if it does not exist. Other get methods (except
* getInstance) return null if the data does not exist. In addition, this
* method is not really useful to external code and probably should never
* have been exposed as a public API in the first place; external code should
* never need to force the creation of a ConversationContext.
* <p>
* For internal use within this class, use either findConversationContextId()
* or getOrCreateConversationContextId().
* <p>
* To just obtain the current ConversationContext <i>if it exists</i>, see
* method getCurrentConversationContext().
*
* @deprecated This method should not be needed by external classes, and
* was inconsistent with other methods on this class.
*/
public Long getConversationContextId()
{
return getOrCreateConversationContextId();
}
/**
* Allocate a new Long value for use as a conversation context id.
* <p>
* The returned value must not match any conversation context id already in
* use within this ConversationManager instance (which is scoped to the
* current http session).
*/
private Long createNextConversationContextId()
{
Long conversationContextId;
synchronized(this)
{
conversationContextId = new Long(nextConversationContextId);
nextConversationContextId++;
}
return conversationContextId;
}
/**
* Get the conversation context for the given id.
* <p>
* Null is returned if there is no ConversationContext with the specified id.
* <p>
* Param conversationContextId must not be null.
* <p>
* Public since version 1.3.
*/
public ConversationContext getConversationContext(Long conversationContextId)
{
synchronized (this)
{
return (ConversationContext) conversationContexts.get(conversationContextId);
}
}
/**
* Get the conversation context for the given id.
* <p>
* If there is no such conversation context a new one will be created.
* The new conversation context will be a "top-level" context (ie has no parent).
* <p>
* The new conversation context will <i>not</i> be the current conversation context,
* unless the id passed in was already configured as the current conversation context id.
*/
protected ConversationContext getOrCreateConversationContext(Long conversationContextId)
{
synchronized (this)
{
ConversationContext conversationContext = (ConversationContext) conversationContexts.get(
conversationContextId);
if (conversationContext == null)
{
ConversationContextFactory factory = FactoryFinder.getConversationContextFactory();
conversationContext = factory.createConversationContext(null, conversationContextId.longValue());
conversationContexts.put(conversationContextId, conversationContext);
// TODO: add the "user" name here, otherwise this debugging is not very useful
// except when testing a webapp with only one user.
log.debug("Created context " + conversationContextId);
}
return conversationContext;
}
}
/**
* This will create a new conversation context using the specified context as
* its parent.
* <p>
* The returned context is not selected as the "current" one; see activateConversationContext.
*
* @since 1.3
*/
public ConversationContext createConversationContext(ConversationContext parent)
{
Long ctxId = createNextConversationContextId();
ConversationContextFactory factory = FactoryFinder.getConversationContextFactory();
ConversationContext ctx = factory.createConversationContext(parent, ctxId.longValue());
synchronized(this)
{
conversationContexts.put(ctxId, ctx);
}
return ctx;
}
/**
* Make the specific context the current context for the current HTTP session.
* <p>
* Methods like getCurrentConversationContext will then return the specified
* context object.
*
* @since 1.2
*/
public void activateConversationContext(ConversationContext ctx)
{
FrameworkAdapter fa = FrameworkAdapter.getCurrentInstance();
fa.setRequestAttribute(CONVERSATION_CONTEXT_REQ, ctx.getIdAsLong());
}
/**
* Ends all conversations within the current context; the context itself will remain active.
*/
public void clearCurrentConversationContext()
{
Long conversationContextId = findConversationContextId();
if (conversationContextId != null)
{
ConversationContext conversationContext = getConversationContext(conversationContextId);
if (conversationContext != null)
{
conversationContext.invalidate();
}
}
}
/**
* Removes the specified contextId from the set of known contexts,
* and deletes every conversation in it.
* <p>
* Objects in the conversation which implement ConversationAware
* will have callbacks invoked.
* <p>
* The conversation being removed must not be the currently active
* context. If it is, then method activateConversationContext should
* first be called on some other instance (perhaps the parent of the
* one being removed) before this method is called.
*
* @since 1.3
*/
public void removeAndInvalidateConversationContext(ConversationContext context)
{
if (context.hasChildren())
{
throw new OrchestraException("Cannot remove context with children");
}
if (context.getIdAsLong().equals(findConversationContextId()))
{
throw new OrchestraException("Cannot remove current context");
}
synchronized(conversationContexts)
{
conversationContexts.remove(context.getIdAsLong());
}
ConversationContext parent = context.getParent();
if (parent != null)
{
parent.removeChild(context);
}
context.invalidate();
// TODO: add the deleted context ids to a list stored in the session,
// and redirect to an error page if any future request specifies this id.
// This catches things like going "back" into a flow that has ended, or
// navigating with the parent page of a popup flow (which kills the popup
// flow context) then trying to use the popup page.
//
// We cannot simply report an error for every case where an invalid id is
// used, because bookmarks will have ids in them; when the bookmark is used
// after the session has died we still want the bookmark url to work. Possibly
// we should allow GET with a bad id, but always fail a POST with one?
}
/**
* Removes the specified contextId from the set of known contexts.
* <p>
* It does nothing else. Maybe it should be called "detachConversationContext"
* or similar.
*
* @deprecated This method is not actually used by anything.
*/
protected void removeConversationContext(Long conversationContextId)
{
synchronized (this)
{
conversationContexts.remove(conversationContextId);
}
}
/**
* Start a conversation.
*
* @see ConversationContext#startConversation(String, ConversationFactory)
*/
public Conversation startConversation(String name, ConversationFactory factory)
{
ConversationContext conversationContext = getOrCreateCurrentConversationContext();
return conversationContext.startConversation(name, factory);
}
/**
* Remove a conversation
*
* Note: It is assumed that the conversation has already been invalidated
*
* @see ConversationContext#removeConversation(String)
*/
protected void removeConversation(String name)
{
Long conversationContextId = findConversationContextId();
if (conversationContextId != null)
{
ConversationContext conversationContext = getConversationContext(conversationContextId);
if (conversationContext != null)
{
conversationContext.removeConversation(name);
}
}
}
/**
* Get the conversation with the given name
*
* @return null if no conversation context is active or if the conversation did not exist.
*/
public Conversation getConversation(String name)
{
ConversationContext conversationContext = getCurrentConversationContext();
if (conversationContext == null)
{
return null;
}
return conversationContext.getConversation(name);
}
/**
* check if the given conversation is active
*/
public boolean hasConversation(String name)
{
ConversationContext conversationContext = getCurrentConversationContext();
if (conversationContext == null)
{
return false;
}
return conversationContext.hasConversation(name);
}
/**
* Returns an iterator over all the Conversation objects in the current conversation
* context. Never returns null, even if no conversation context exists.
*/
public Iterator iterateConversations()
{
ConversationContext conversationContext = getCurrentConversationContext();
if (conversationContext == null)
{
return EMPTY_ITERATOR;
}
return conversationContext.iterateConversations();
}
/**
* Get the current conversation context.
* <p>
* In a simple Orchestra application this will always be a root conversation context.
* When using a dialog/page-flow environment the context that is returned might have
* a parent context.
* <p>
* Null is returned if there is no current conversationContext.
*/
public ConversationContext getCurrentConversationContext()
{
Long ccid = findConversationContextId();
if (ccid == null)
{
return null;
}
else
{
ConversationContext ctx = getConversationContext(ccid);
if (ctx == null)
{
// Someone has perhaps used the back button to go back into a context
// that has already ended. This simply will not work, so we should
// throw an exception here.
//
// Or somebody might have just activated a bookmark. Unfortunately,
// when someone bookmarks a page within an Orchestra app, the bookmark
// will capture the contextId too.
//
// There is unfortunately no obvious way to tell these two actions apart.
// So we cannot report an error here; instead, just return a null context
// so that a new instance gets created - and hope that the page itself
// detects the problem and reports an error if it needs conversation state
// that does not exist.
//
// What we should do here *at least* is bump the nextConversationId value
// to be greater than this value, so that we don't later try to allocate a
// second conversation with the same id. Yes, evil users could pass a very
// high value here and cause wraparound but that is really not a problem as
// they can only screw themselves up.
log.warn("ConversationContextId specified but context does not exist");
synchronized(this)
{
if (nextConversationContextId <= ccid.longValue())
{
nextConversationContextId = ccid.longValue() + 1;
}
}
return null;
}
return ctx;
}
}
/**
* Return the current ConversationContext for the current http session;
* if none yet exists then a ConversationContext is created and configured
* as the current context.
* <p>
* This is currently package-scoped because it is not clear that code
* outside orchestra can have any use for this method. The only user
* outside of this class is ConversationRequestParameterProvider.
*
* @since 1.2
*/
ConversationContext getOrCreateCurrentConversationContext()
{
Long ccid = getOrCreateConversationContextId();
return getOrCreateConversationContext(ccid);
}
/**
* Return true if there is a conversation context associated with the
* current request.
*/
public boolean hasConversationContext()
{
return getCurrentConversationContext() == null;
}
/**
* Get the current root conversation context (aka the window conversation context).
* <p>
* Null is returned if it does not exist.
*
* @since 1.2
*/
public ConversationContext getCurrentRootConversationContext()
{
Long ccid = findConversationContextId();
if (ccid == null)
{
return null;
}
synchronized (this)
{
ConversationContext conversationContext = getConversationContext(ccid);
if (conversationContext == null)
{
return null;
}
else
{
return conversationContext.getRoot();
}
}
}
/**
* Get the Messager used to inform the user about anomalies.
* <p>
* What instance is returned is controlled by the FrameworkAdapter. See
* {@link org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter} for details.
*/
public ConversationMessager getMessager()
{
return FrameworkAdapter.getCurrentInstance().getConversationMessager();
}
/**
* Check the timeout for each conversation context, and all conversations
* within those contexts.
* <p>
* If any conversation has not been accessed within its timeout period
* then clear the context.
* <p>
* Invoke the checkTimeout method on each context so that any conversation
* that has not been accessed within its timeout is invalidated.
*/
protected void checkTimeouts()
{
Map.Entry[] contexts;
synchronized (this)
{
contexts = new Map.Entry[conversationContexts.size()];
conversationContexts.entrySet().toArray(contexts);
}
long checkTime = System.currentTimeMillis();
for (int i = 0; i<contexts.length; i++)
{
Map.Entry context = contexts[i];
ConversationContext conversationContext = (ConversationContext) context.getValue();
if (conversationContext.hasChildren())
{
// Never time out contexts that have children. Let the children time out first...
continue;
}
conversationContext.checkConversationTimeout();
if (conversationContext.getTimeout() > -1 &&
(conversationContext.getLastAccess() +
conversationContext.getTimeout()) < checkTime)
{
if (log.isDebugEnabled())
{
log.debug("end conversation context due to timeout: " + conversationContext.getId());
}
removeAndInvalidateConversationContext(conversationContext);
}
}
}
/**
* @since 1.4
*/
public void removeAndInvalidateAllConversationContexts()
{
ConversationContext[] contexts;
synchronized (this)
{
contexts = new ConversationContext[conversationContexts.size()];
conversationContexts.values().toArray(contexts);
}
for (int i = 0; i<contexts.length; i++)
{
ConversationContext context = contexts[i];
removeAndInvalidateConversationContextAndChildren(context);
}
}
private void removeAndInvalidateConversationContextAndChildren(ConversationContext conversationContext)
{
while (conversationContext.hasChildren())
{
// Get first child
ConversationContext child = (ConversationContext) conversationContext.getChildren().iterator().next();
// This call removes child from conversationContext.children
removeAndInvalidateConversationContextAndChildren(child);
}
if (log.isDebugEnabled())
{
log.debug("end conversation context: " + conversationContext.getId());
}
removeAndInvalidateConversationContext(conversationContext);
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException
{
// the conversation manager is not (yet) serializable, we just implement it
// to make it work with distributed sessions
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
{
// nothing written, so nothing to read
}
private Object readResolve() throws ObjectStreamException
{
// Note: Returning null here is not a good idea (for Tomcat 6.0.16 at least). Null objects are
// not permitted within an HttpSession; calling HttpSession.setAttribute(name, null) is defined as
// removing the attribute. So returning null here when deserializing an object from the session
// can cause problems.
//
// Note that nothing should have a reference to the ConversationManager *except* the entry
// in the http session; all other code should look it up "on demand" via the getInstance
// method rather than storing a reference to it. So we can do pretty much anything we like
// here as long as the getInstance() method works correctly later. Thus:
// * returning null here is one option (getInstance just creates the item later) - except
// that tomcat doesn't like it.
// * creating a new object instance that getInstance will later simply find and return will
// work - except that the actual type to create can be overridden via the dependency-injection
// config, and the FrameworkAdapter class that gives us access to that info is not available
// at the current time.
//
// To solve this, we use a hack: a special DUMMY object is returned (and therefore will be inserted
// into the HTTP session under the ConversationManager key). The getInstance method then checks
// for this dummy object, and treats it like NULL. Conveniently, it appears that the serialization
// mechanism doesn't care if readResolve returns an object that is not a subclass of the one that
// is being deserialized, so here we can return any old object (eg an Integer).
//
// An alternative would be to just remove the ConversationManager object from the http session
// on passivate, so that this readResolve method is never called. However hopefully at some
// future time we *will* get serialization for this class working nicely and then will need
// to discard these serialization hacks; it is easier to do that when the hacks are all in
// the same class.
Log log = LogFactory.getLog(ConversationManager.class);
log.debug("readResolve returning dummy ConversationManager object");
return DUMMY;
}
}