/*
* 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.lib.OrchestraException;
import org.apache.myfaces.orchestra.lib._ReentrantLock;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.Collection;
/**
* A ConversationContext is a container for a set of conversations.
* <p>
* Normally there is only one ConversationContext per http session. However there can
* be multiple instances if the user has multiple concurrent windows open into the same
* webapp, using the ox:separateConversationContext or other similar mechanism.
* <p>
* Like the conversation class, a context can also have a timeout which will cause it
* to be ended automatically if not accessed within the given period.
*/
public class ConversationContext
{
private final Log log = LogFactory.getLog(ConversationContext.class);
// This id is attached as a query parameter to every url rendered in a page
// (forms and links) so that if that url is invoked then the request will
// cause the same context to be used from the user's http session.
//
// This value is never null, but an Object is used to store it rather than
// a primitive long because it is used as a key into a collection of
// conversation contexts, and using an object here saves wrapping this
// value in a new object instance every time it must be used as a key.
private final Long id;
// See addAttribute
private final Map attributes = new TreeMap();
// the parent conversation context
private final ConversationContext parent;
// The conversations held by this context, keyed by conversation name.
private final Map conversations = new TreeMap();
/**
* A name associated with this context
*/
private String name;
// time at which this was last accessed, used for timeouts.
private long lastAccess;
// default timeout for contexts: 30 minutes.
private long timeoutMillis = 30 * 60 * 1000;
private final _ReentrantLock lock = new _ReentrantLock();
// Map of all child contexts of this context, keyed by child.id
private Map childContexts = new HashMap();
/**
* Constructor.
*/
protected ConversationContext(long id)
{
this(null, id);
}
/**
* Constructor.
*
* @since 1.2
*/
protected ConversationContext(ConversationContext parent, long id)
{
this.parent = parent;
this.id = Long.valueOf(id);
if (parent != null)
{
parent.addChild(this);
}
touch();
}
/**
* Get the name associated to this context.
*
* @since 1.2
*/
public String getName()
{
return name;
}
/**
* Assign a name to this context.
*
* @since 1.2
*/
public void setName(String name)
{
this.name = name;
}
/**
* The conversation context id, unique within the current http session.
*/
public long getId()
{
return id.longValue();
}
/**
* The conversation context id, unique within the current http session.
*/
public Long getIdAsLong()
{
return id;
}
/**
* Return the parent conversation context (if any).
*
* @since 1.2
*/
public ConversationContext getParent()
{
return parent;
}
/**
* @since 1.3
*/
public void addChild(ConversationContext context)
{
childContexts.put(context.getIdAsLong(), context);
}
/**
* @since 1.4
*/
protected Collection getChildren()
{
return childContexts.values();
}
/**
* @since 1.3
*/
public void removeChild(ConversationContext context)
{
Object o = childContexts.remove(context.getIdAsLong());
if (o != context)
{
// Sanity check failed: o is null, or o is a different object.
// In either case, something is very wrong.
throw new OrchestraException("Invalid call of removeChild");
}
}
/**
* @since 1.3
*/
public boolean hasChildren()
{
return !childContexts.isEmpty();
}
/**
* Mark this context as having been used.
*/
protected void touch()
{
lastAccess = System.currentTimeMillis();
if (getParent() != null)
{
getParent().touch();
}
}
/**
* The system time in millis when this conversation has been accessed last.
*/
public long getLastAccess()
{
return lastAccess;
}
/**
* Get the timeout after which this context will be closed.
*
* @see #setTimeout
*/
public long getTimeout()
{
return timeoutMillis;
}
/**
* Set the timeout after which this context will be closed.
* <p>
* A value of -1 means no timeout checking.
*/
public void setTimeout(long timeoutMillis)
{
this.timeoutMillis = timeoutMillis;
}
/**
* Invalidate all conversations within this context.
*
* @deprecated Use the "invalidate" method instead.
*/
protected void clear()
{
invalidate();
}
/**
* Invalidate all conversations within this context.
*
* @since 1.3
*/
protected void invalidate()
{
synchronized (this)
{
Conversation[] convArray = new Conversation[conversations.size()];
conversations.values().toArray(convArray);
for (int i = 0; i < convArray.length; i++)
{
Conversation conversation = convArray[i];
conversation.invalidate();
}
conversations.clear();
}
}
/**
* Start a conversation if not already started.
*/
protected Conversation startConversation(String name, ConversationFactory factory)
{
synchronized (this)
{
touch();
Conversation conversation = (Conversation) conversations.get(name);
if (conversation == null)
{
conversation = factory.createConversation(this, name);
conversations.put(name, conversation);
}
return conversation;
}
}
/**
* Remove the conversation from this context.
*
* <p>Notice: It is assumed that the conversation has already been invalidated.</p>
*/
protected void removeConversation(Conversation conversation)
{
synchronized (this)
{
touch();
conversations.remove(conversation.getName());
}
}
/**
* Remove the conversation with the given name from this context.
*
* <p>Notice: Its assumed that the conversation has already been invalidated</p>
*/
protected void removeConversation(String name)
{
synchronized (this)
{
touch();
Conversation conversation = (Conversation) conversations.get(name);
if (conversation != null)
{
removeConversation(conversation);
}
}
}
/**
* Return true if there are one or more conversations in this context.
*/
protected boolean hasConversations()
{
synchronized (this)
{
touch();
return conversations.size() > 0;
}
}
/**
* Check if the given conversation exists.
*/
protected boolean hasConversation(String name)
{
synchronized (this)
{
touch();
return conversations.get(name) != null;
}
}
/**
* Get a conversation by name.
* <p>
* This looks only in the current context, not in any child contexts.
*/
protected Conversation getConversation(String name)
{
synchronized (this)
{
touch();
Conversation conv = (Conversation) conversations.get(name);
if (conv != null)
{
conv.touch();
}
return conv;
}
}
/**
* Iterates over all the conversations in this context.
* <p>
* This does not include conversations in parent contexts.
*
* @return An iterator over a copy of the conversation list. It is safe to remove objects from
* the conversation list while iterating, as the iterator refers to a different collection.
*/
public Iterator iterateConversations()
{
synchronized (this)
{
touch();
Conversation[] convs = (Conversation[]) conversations.values().toArray(
new Conversation[conversations.size()]);
return Arrays.asList(convs).iterator();
}
}
/**
* Check the timeout for every conversation in this context.
* <p>
* This method does not check the timeout for this context object itself.
*/
protected void checkConversationTimeout()
{
synchronized (this)
{
Conversation[] convArray = new Conversation[conversations.size()];
conversations.values().toArray(convArray);
for (int i = 0; i < convArray.length; i++)
{
Conversation conversation = convArray[i];
ConversationTimeoutableAspect timeoutAspect =
(ConversationTimeoutableAspect)
conversation.getAspect(ConversationTimeoutableAspect.class);
if (timeoutAspect != null && timeoutAspect.isTimeoutReached())
{
if (log.isDebugEnabled())
{
log.debug("end conversation due to timeout: " + conversation.getName());
}
conversation.invalidate();
}
}
}
}
/**
* Add an attribute to the conversationContext.
* <p>
* A context provides a map into which any arbitrary objects can be stored. It
* isn't a major feature of the context, but can occasionally be useful.
*/
public void setAttribute(String name, Object attribute)
{
synchronized(attributes)
{
attributes.remove(name);
attributes.put(name, attribute);
}
}
/**
* Check if this conversationContext holds a specific attribute.
*/
public boolean hasAttribute(String name)
{
synchronized(attributes)
{
return attributes.containsKey(name);
}
}
/**
* Get a specific attribute.
*/
public Object getAttribute(String name)
{
synchronized(attributes)
{
return attributes.get(name);
}
}
/**
* Remove an attribute from the conversationContext.
*/
public Object removeAttribute(String name)
{
synchronized(attributes)
{
return attributes.remove(name);
}
}
/**
* Block until no other thread has this instance marked as reserved, then
* mark the object as reserved for this thread.
* <p>
* It is safe to call this method multiple times.
* <p>
* If this method is called, then an equal number of calls to
* unlockForCurrentThread <b>MUST</b> made, or this context object
* will remain locked until the http session times out.
* <p>
* Note that this method may be called very early in the request processing
* lifecycle, eg before a FacesContext exists for a JSF request.
*
* @since 1.1
*/
public void lockInterruptablyForCurrentThread() throws InterruptedException
{
if (log.isDebugEnabled())
{
log.debug("Locking context " + this.id);
}
lock.lockInterruptibly();
}
/**
* Block until no other thread has this instance marked as reserved, then
* mark the object as reserved for this thread.
* <p>
* Note that this method may be called very late in the request processing
* lifecycle, eg after a FacesContext has been destroyed for a JSF request.
*
* @since 1.1
*/
public void unlockForCurrentThread()
{
if (log.isDebugEnabled())
{
log.debug("Unlocking context " + this.id);
}
lock.unlock();
}
/**
* Return true if this object is currently locked by the calling thread.
*
* @since 1.1
*/
public boolean isLockedForCurrentThread()
{
return lock.isHeldByCurrentThread();
}
/**
* Get the root conversation context this conversation context is
* associated with.
* <p>
* This is equivalent to calling getParent repeatedly until a context
* with no parent is found.
*
* @since 1.2
*/
public ConversationContext getRoot()
{
ConversationContext cctx = this;
while (cctx != null && cctx.getParent() != null)
{
cctx = getParent();
}
return cctx;
}
}