/*
* 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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
import org.apache.myfaces.orchestra.requestParameterProvider.RequestParameterProviderManager;
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;
/**
* Deals with the various conversation contexts in the current session.
* <p>
* A new conversation context will be created if the servlet request did
* not specify an existing conversation context id.
* <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();
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. This creates a new one if none exists.
*/
public static ConversationManager getInstance()
{
return getInstance(true);
}
/**
* Get the conversation manager.
* <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 should not created one, it doesn't matter if there is no
// FrameworkAdapter available.
return null;
}
else
{
throw new IllegalStateException("FrameworkAdapter not found");
}
}
ConversationManager conversationManager = (ConversationManager) frameworkAdapter.getSessionAttribute(
CONVERSATION_MANAGER_KEY);
if (conversationManager == null && create)
{
// TODO: do not call new directly here, as it makes it impossible to configure
// an alternative ConversationManager instance. This is IOC and test unfriendly.
conversationManager = new ConversationManager();
// 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.
*/
protected 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)
{
conversationContext = new ConversationContext(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;
}
}
/**
* 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.clear();
}
}
}
/**
* Destroy the given conversation context.
* <p>
* Note: it is assumed that the context is already been destroyed
*/
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
{
return getConversationContext(ccid);
}
}
/**
* 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];
Long conversationContextId = (Long) context.getKey();
ConversationContext conversationContext = (ConversationContext) context.getValue();
conversationContext.checkConversationTimeout();
if (conversationContext.getTimeout() > -1 &&
(conversationContext.getLastAccess() +
conversationContext.getTimeout()) < checkTime)
{
if (log.isDebugEnabled())
{
log.debug("end conversation context due to timeout: " + conversationContext.getId());
}
conversationContext.clear();
synchronized (this)
{
conversationContexts.remove(conversationContextId);
}
}
}
}
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
{
// do not return a real object, that way on first request a new conversation manager will be created
return null;
}
}