blob: 717ae2657c4dca168112f0c9e7fc5297cd7bd0b8 [file] [log] [blame]
/*
* 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.cocoon.components.flow;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import org.apache.avalon.framework.component.Component;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.context.Context;
import org.apache.avalon.framework.context.ContextException;
import org.apache.avalon.framework.context.Contextualizable;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.avalon.framework.service.Serviceable;
import org.apache.avalon.framework.thread.ThreadSafe;
import org.apache.cocoon.components.ContextHelper;
import org.apache.cocoon.components.thread.RunnableManager;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.environment.Session;
import org.apache.cocoon.util.Deprecation;
import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.iterators.FilterIterator;
import org.apache.excalibur.instrument.CounterInstrument;
import org.apache.excalibur.instrument.Instrument;
import org.apache.excalibur.instrument.Instrumentable;
import org.apache.excalibur.instrument.ValueInstrument;
/**
* The default implementation of {@link ContinuationsManager}. <br/>There are
* two modes of work: <br/>
* <ul>
* <li><b>standard mode </b>- continuations are stored in single holder. No
* security is applied to continuation lookup. Anyone can invoke a continuation
* only knowing the ID. Set "session-bound-continuations" configuration option
* to false to activate this mode.</li>
* <li><b>secure mode </b>- each session has it's own continuations holder. A
* continuation is only valid for the same session it was created for. Session
* invalidation causes all bound continuations to be invalidated as well. Use
* this setting for web applications. Set "session-bound-continuations"
* configuration option to true to activate this mode.</li>
* </ul>
*
* @author <a href="mailto:ovidiu@cup.hp.com">Ovidiu Predescu </a>
* @author <a href="mailto:Michael.Melhem@managesoft.com">Michael Melhem </a>
* @since March 19, 2002
* @see ContinuationsManager
* @version CVS $Id$
*/
public class ContinuationsManagerImpl
extends AbstractLogEnabled
implements ContinuationsManager, Component, Configurable,
ThreadSafe, Instrumentable, Serviceable, Contextualizable {
private static final int CONTINUATION_ID_LENGTH = 20;
/**
* Random number generator used to create continuation ID
*/
protected SecureRandom random;
protected final byte[] bytes = new byte[CONTINUATION_ID_LENGTH];
/**
* Sorted set of <code>WebContinuation</code> instances, based on
* their expiration time. This is used by the background thread to
* invalidate continuations.
*/
protected final SortedSet expirations = Collections.synchronizedSortedSet(new TreeSet());
protected ServiceManager serviceManager;
protected Context context;
/**
* How long does a continuation exist in memory since the last
* access? The time is in milliseconds, and the default is 1 hour.
*/
protected int defaultTimeToLive;
protected boolean isContinuationSharingBugCompatible;
protected boolean bindContinuationsToSession;
/**
* Main continuations holder. Used unless continuations are stored in user
* session.
*/
protected WebContinuationsHolder continuationsHolder;
// Instrumentation
protected String instrumentableName;
protected ValueInstrument continuationsCount;
protected int continuationsCounter;
protected ValueInstrument expirationsSize;
protected CounterInstrument continuationsCreated;
protected CounterInstrument continuationsInvalidated;
public ContinuationsManagerImpl() throws Exception {
try {
random = SecureRandom.getInstance("SHA1PRNG");
} catch(java.security.NoSuchAlgorithmException nsae) {
// Maybe we are on IBM's SDK
random = SecureRandom.getInstance("IBMSecureRandom");
}
random.setSeed(System.currentTimeMillis());
continuationsCount = new ValueInstrument("count");
continuationsCounter = 0;
expirationsSize = new ValueInstrument("expirations-size");
continuationsCreated = new CounterInstrument("creates");
continuationsInvalidated = new CounterInstrument("invalidates");
}
public void contextualize(Context context) throws ContextException {
this.context = context;
}
public void service(ServiceManager manager) throws ServiceException {
this.serviceManager = manager;
}
public void configure(Configuration config) {
this.defaultTimeToLive = config.getAttributeAsInteger("time-to-live", (3600 * 1000));
this.isContinuationSharingBugCompatible = config.getAttributeAsBoolean("continuation-sharing-bug-compatible", false);
this.bindContinuationsToSession = config.getAttributeAsBoolean( "session-bound-continuations", false );
// create a global ContinuationsHolder if this the "session-bound-continuations" parameter is set to false
if (!this.bindContinuationsToSession) {
this.continuationsHolder = new WebContinuationsHolder();
}
// create a thread that invalidates the continuations
final Configuration expireConf = config.getChild("expirations-check");
final long initialDelay = expireConf.getChild("offset", true).getValueAsLong(180000);
final long interval = expireConf.getChild("period", true).getValueAsLong(180000);
try {
final RunnableManager runnableManager = (RunnableManager)serviceManager.lookup(RunnableManager.ROLE);
runnableManager.execute( new Runnable() {
public void run()
{
expireContinuations();
}
}, initialDelay, interval);
serviceManager.release(runnableManager);
} catch (Exception e) {
getLogger().warn("Could not enqueue continuations expiration task. " +
"Continuations will not automatically expire.", e);
}
}
public void setInstrumentableName(String instrumentableName) {
this.instrumentableName = instrumentableName;
}
public String getInstrumentableName() {
return instrumentableName;
}
public Instrument[] getInstruments() {
return new Instrument[]{
continuationsCount,
continuationsCreated,
continuationsInvalidated
};
}
public Instrumentable[] getChildInstrumentables() {
return Instrumentable.EMPTY_INSTRUMENTABLE_ARRAY;
}
public WebContinuation createWebContinuation(Object kont,
WebContinuation parent,
int timeToLive,
String interpreterId,
ContinuationsDisposer disposer) {
int ttl = timeToLive == 0 ? defaultTimeToLive : timeToLive;
WebContinuation wk = generateContinuation(kont, parent, ttl, interpreterId, disposer);
synchronized (this.expirations) {
if (parent != null) {
expirations.remove(parent);
}
expirations.add(wk);
expirationsSize.setValue(expirations.size());
}
if (getLogger().isDebugEnabled()) {
getLogger().debug("WK: Created continuation " + wk.getId());
}
return wk;
}
public WebContinuation lookupWebContinuation(String id, String interpreterId) {
WebContinuationsHolder continuationsHolder = lookupWebContinuationsHolder(false);
if (continuationsHolder == null) {
return null;
}
WebContinuation kont = continuationsHolder.get(id);
if (kont == null) {
return null;
}
if (kont.hasExpired()) {
removeContinuation(continuationsHolder, kont);
return null;
}
if (!kont.interpreterMatches(interpreterId)) {
if (getLogger().isWarnEnabled()) {
getLogger().warn("WK: Continuation (" + kont.getId()
+ ") lookup for wrong interpreter. Bound to: "
+ kont.getInterpreterId() + ", looked up for: "
+ interpreterId);
}
if (!isContinuationSharingBugCompatible) {
return null;
}
}
// COCOON-2109: Sorting in the TreeSet happens on insert. So in order to re-sort the
// continuation has to be re-added.
synchronized (this.expirations) {
this.expirations.remove(kont);
kont.updateLastAccessTime();
this.expirations.add(kont);
}
return kont;
}
/**
* Create <code>WebContinuation</code> and generate unique identifier
* for it. The identifier is generated using a cryptographically strong
* algorithm to prevent people to generate their own identifiers.
*
* @param kont an <code>Object</code> value representing continuation
* @param parent value representing parent <code>WebContinuation</code>
* @param ttl <code>WebContinuation</code> time to live
* @param interpreterId id of interpreter invoking continuation creation
* @param disposer <code>ContinuationsDisposer</code> instance to use for
* cleanup of the continuation.
* @return the generated <code>WebContinuation</code> with unique identifier
*/
protected WebContinuation generateContinuation(Object kont,
WebContinuation parent,
int ttl,
String interpreterId,
ContinuationsDisposer disposer) {
char[] result = new char[bytes.length * 2];
WebContinuation wk;
WebContinuationsHolder continuationsHolder = lookupWebContinuationsHolder(true);
while (true) {
random.nextBytes(bytes);
for (int i = 0; i < CONTINUATION_ID_LENGTH; i++) {
byte ch = bytes[i];
result[2 * i] = Character.forDigit(Math.abs(ch >> 4), 16);
result[2 * i + 1] = Character.forDigit(Math.abs(ch & 0x0f), 16);
}
final String id = new String(result);
synchronized (continuationsHolder.holder) {
if (!continuationsHolder.contains(id)) {
if (this.bindContinuationsToSession) {
wk = new HolderAwareWebContinuation(id, kont, parent,
ttl, interpreterId, disposer,
continuationsHolder);
} else {
wk = new WebContinuation(id, kont, parent, ttl,
interpreterId, disposer);
}
continuationsHolder.addContinuation(wk);
synchronized (continuationsCount) {
continuationsCounter++;
continuationsCount.setValue(continuationsCounter);
}
break;
}
}
}
continuationsCreated.increment();
return wk;
}
public void invalidateWebContinuation(WebContinuation wk) {
WebContinuationsHolder continuationsHolder = lookupWebContinuationsHolder(false);
if (!continuationsHolder.contains(wk)) {
//TODO this looks like a security breach - should we throw?
return;
}
_detach(wk);
_invalidate(continuationsHolder, wk);
}
private void _invalidate(WebContinuationsHolder continuationsHolder, WebContinuation wk) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("WK: Manual expire of continuation " + wk.getId());
}
disposeContinuation(continuationsHolder, wk);
synchronized (this.expirations) {
expirations.remove(wk);
expirationsSize.setValue(expirations.size());
}
// Invalidate all the children continuations as well
List children = wk.getChildren();
int size = children.size();
for (int i = 0; i < size; i++) {
_invalidate(continuationsHolder, (WebContinuation) children.get(i));
}
}
/**
* Detach this continuation from parent. This method removes
* continuation from parent's children collection, if it has parent.
* @param wk Continuation to detach from parent.
*/
private void _detach(WebContinuation wk) {
WebContinuation parent = wk.getParentContinuation();
if (parent != null) {
wk.detachFromParent();
}
}
/**
* Makes the continuation inaccessible for lookup, and triggers possible needed
* cleanup code through the ContinuationsDisposer interface.
* @param continuationsHolder
*
* @param wk the continuation to dispose.
*/
protected void disposeContinuation(WebContinuationsHolder continuationsHolder, WebContinuation wk) {
continuationsHolder.removeContinuation(wk);
synchronized( continuationsCount ) {
continuationsCounter--;
continuationsCount.setValue(continuationsCounter);
}
wk.dispose();
continuationsInvalidated.increment();
}
/**
* Removes an expired leaf <code>WebContinuation</code> node
* from its continuation tree, and recursively removes its
* parent(s) if it they have expired and have no (other) children.
* @param continuationsHolder
*
* @param wk <code>WebContinuation</code> node
*/
protected void removeContinuation(WebContinuationsHolder continuationsHolder, WebContinuation wk) {
if (wk.getChildren().size() != 0) {
return;
}
// remove access to this continuation
disposeContinuation(continuationsHolder, wk);
_detach(wk);
if (getLogger().isDebugEnabled()) {
getLogger().debug("WK: Deleted continuation: " + wk.getId());
}
// now check if parent needs to be removed.
WebContinuation parent = wk.getParentContinuation();
if (null != parent && parent.hasExpired()) {
//parent must have the same continuations holder, lookup not needed
removeContinuation(continuationsHolder, parent);
}
}
/**
* Remove all continuations which have already expired.
*/
protected void expireContinuations() {
long now = 0;
if (getLogger().isDebugEnabled()) {
now = System.currentTimeMillis();
/* Continuations before clean up: */
displayAllContinuations();
displayExpireSet();
}
// Clean up expired continuations
int count = 0;
FilterIterator expirationIterator = new FilterIterator();
Predicate expirationPredicate = new ExpirationPredicate();
expirationIterator.setPredicate(expirationPredicate);
synchronized (this.expirations) {
expirationIterator.setIterator(this.expirations.iterator());
while (expirationIterator.hasNext()) {
WebContinuation wk = (WebContinuation) expirationIterator.next();
expirationIterator.remove();
WebContinuationsHolder continuationsHolder;
if (wk instanceof HolderAwareWebContinuation) {
continuationsHolder = ((HolderAwareWebContinuation) wk).getContinuationsHolder();
} else {
continuationsHolder = this.continuationsHolder;
}
removeContinuation(continuationsHolder, wk);
count++;
}
expirationsSize.setValue(expirations.size());
}
if (getLogger().isDebugEnabled()) {
getLogger().debug("WK Cleaned up " + count + " continuations in " +
(System.currentTimeMillis() - now) + " ms");
}
}
/**
* Method used by WebContinuationsHolder to notify the continuations manager
* about session invalidation. Invalidates all continuations held by passed
* continuationsHolder.
*/
protected void invalidateContinuations(WebContinuationsHolder continuationsHolder) {
// It's not possible to just iterate over continuationsHolder.holder since _invalidate(..)
// calls remove(..) on the map leading to ConcurrentModification at the end.
WebContinuation[] continuations;
synchronized (continuationsHolder.holder) {
continuations = new WebContinuation[continuationsHolder.holder.size()];
continuations = (WebContinuation[]) continuationsHolder.holder.values().toArray(continuations);
}
for (int i = 0; i < continuations.length; i++) {
_detach(continuations[i]);
_invalidate(continuationsHolder, continuations[i]);
}
}
/**
* Lookup a proper web continuations holder.
* @param createNew
* should the manager create a continuations holder in session
* when none found?
*/
public WebContinuationsHolder lookupWebContinuationsHolder(boolean createNew) {
//there is only one holder if continuations are not bound to session
if (!this.bindContinuationsToSession)
return this.continuationsHolder;
//if continuations bound to session lookup a proper holder in the session
Map objectModel = ContextHelper.getObjectModel(this.context);
Request request = ObjectModelHelper.getRequest(objectModel);
if (!createNew && request.getSession(false) == null)
return null;
Session session = request.getSession(true);
WebContinuationsHolder holder =
(WebContinuationsHolder) session.getAttribute(
WebContinuationsHolder.CONTINUATIONS_HOLDER);
if (!createNew)
return holder;
if (holder != null)
return holder;
holder = new WebContinuationsHolder();
session.setAttribute(WebContinuationsHolder.CONTINUATIONS_HOLDER, holder);
return holder;
}
public Set getForest() {
Set rootWebContinuations = new HashSet();
// identify the root continuations, once done no more need to lock
synchronized (this.expirations) {
for (Iterator iter = this.expirations.iterator(); iter.hasNext();) {
WebContinuation webContinuation = (WebContinuation) iter.next();
while (webContinuation.getParentContinuation() != null) {
webContinuation = webContinuation.getParentContinuation();
}
rootWebContinuations.add(webContinuation);
}
}
Set clonedRootWebContinuations = new HashSet();
for (Iterator iter = rootWebContinuations.iterator(); iter.hasNext();) {
WebContinuation rootContinuation = (WebContinuation) iter.next();
clonedRootWebContinuations.add(rootContinuation.clone());
}
return clonedRootWebContinuations;
}
/**
* Get a list of all web continuations (data only)
*
* @deprecated
*/
public List getWebContinuationsDataBeanList() {
if (Deprecation.logger.isWarnEnabled()) {
Deprecation.logger.warn("ContinuationsManager.getWebContinuationsDataBeanList()"
+ " is deprecated and should be replaced with getForest().");
}
List beanList = new ArrayList();
for (Iterator it = getForest().iterator(); it.hasNext();) {
beanList.add(new WebContinuationDataBean((WebContinuation) it.next()));
}
return beanList;
}
/**
* Dump to Log file the current contents of
* the expirations <code>SortedSet</code>
*/
protected void displayExpireSet() {
StringBuffer wkSet = new StringBuffer("\nWK; Expire set size: ");
synchronized (this.expirations) {
wkSet.append(this.expirations.size());
for (Iterator i = this.expirations.iterator(); i.hasNext();) {
final WebContinuation wk = (WebContinuation) i.next();
wkSet.append("\nWK: ").append(wk.getId()).append(" ExpireTime [");
if (wk.hasExpired()) {
wkSet.append("Expired");
} else {
wkSet.append(wk.getLastAccessTime() + wk.getTimeToLive());
}
wkSet.append("]");
}
}
getLogger().debug(wkSet.toString());
}
/**
* Dump to Log file all <code>WebContinuation</code>s
* in the system.
*
* This method will be changed to be an internal method solely for debugging
* purposes just like {@link #displayExpireSet()}.
*/
public void displayAllContinuations() {
if (getLogger().isDebugEnabled()) {
Set forest = getForest();
getLogger().debug("WK: Forest size: " + forest.size());
for (Iterator iter = forest.iterator(); iter.hasNext();) {
getLogger().debug(iter.next().toString());
}
}
}
/**
* A holder for WebContinuations. When bound to session notifies the
* continuations manager of session invalidation.
*
* For thread-safe access you have to synchronize on the Map {@link #holder}!
*/
protected class WebContinuationsHolder implements HttpSessionBindingListener {
private final static String CONTINUATIONS_HOLDER = "o.a.c.c.f.SCMI.WebContinuationsHolder";
private Map holder = Collections.synchronizedMap(new HashMap());
public WebContinuation get(Object id) {
return (WebContinuation) this.holder.get(id);
}
public void addContinuation(WebContinuation wk) {
this.holder.put(wk.getId(), wk);
}
public void removeContinuation(WebContinuation wk) {
this.holder.remove(wk.getId());
}
public boolean contains(String continuationId) {
return this.holder.containsKey(continuationId);
}
public boolean contains(WebContinuation wk) {
return contains(wk.getId());
}
public void valueBound(HttpSessionBindingEvent event) {
}
public void valueUnbound(HttpSessionBindingEvent event) {
invalidateContinuations(this);
}
}
/**
* WebContinuation extension that holds also the information about the
* holder. This information is needed to cleanup a proper holder after
* continuation's expiration time.
*/
protected static class HolderAwareWebContinuation extends WebContinuation {
private WebContinuationsHolder continuationsHolder;
public HolderAwareWebContinuation(String id,
Object continuation,
WebContinuation parentContinuation,
int timeToLive,
String interpreterId,
ContinuationsDisposer disposer,
WebContinuationsHolder continuationsHolder) {
super(id, continuation, parentContinuation, timeToLive, interpreterId, disposer);
this.continuationsHolder = continuationsHolder;
}
public WebContinuationsHolder getContinuationsHolder() {
return continuationsHolder;
}
}
protected static class ExpirationPredicate implements Predicate {
public boolean evaluate(final Object obj) {
return ((WebContinuation)obj).hasExpired();
}
}
}