| /* |
| * 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(); |
| } |
| |
| } |
| |
| } |