001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.components.flow;
018:
019: import org.apache.avalon.framework.component.Component;
020: import org.apache.avalon.framework.configuration.Configurable;
021: import org.apache.avalon.framework.configuration.Configuration;
022: import org.apache.avalon.framework.context.Context;
023: import org.apache.avalon.framework.context.ContextException;
024: import org.apache.avalon.framework.context.Contextualizable;
025: import org.apache.avalon.framework.logger.AbstractLogEnabled;
026: import org.apache.avalon.framework.service.ServiceException;
027: import org.apache.avalon.framework.service.ServiceManager;
028: import org.apache.avalon.framework.service.Serviceable;
029: import org.apache.avalon.framework.thread.ThreadSafe;
030: import org.apache.cocoon.components.ContextHelper;
031: import org.apache.cocoon.components.thread.RunnableManager;
032: import org.apache.cocoon.environment.ObjectModelHelper;
033: import org.apache.cocoon.environment.Request;
034: import org.apache.cocoon.environment.Session;
035:
036: import org.apache.excalibur.instrument.CounterInstrument;
037: import org.apache.excalibur.instrument.Instrument;
038: import org.apache.excalibur.instrument.Instrumentable;
039: import org.apache.excalibur.instrument.ValueInstrument;
040:
041: import java.security.SecureRandom;
042: import java.util.ArrayList;
043: import java.util.Collections;
044: import java.util.HashMap;
045: import java.util.HashSet;
046: import java.util.Iterator;
047: import java.util.List;
048: import java.util.Map;
049: import java.util.Set;
050: import java.util.SortedSet;
051: import java.util.TreeSet;
052:
053: import javax.servlet.http.HttpSessionBindingEvent;
054: import javax.servlet.http.HttpSessionBindingListener;
055:
056: /**
057: * The default implementation of {@link ContinuationsManager}. <br/>There are
058: * two modes of work: <br/>
059: * <ul>
060: * <li><b>standard mode </b>- continuations are stored in single holder. No
061: * security is applied to continuation lookup. Anyone can invoke a continuation
062: * only knowing the ID. Set "session-bound-continuations" configuration option
063: * to false to activate this mode.</li>
064: * <li><b>secure mode </b>- each session has it's own continuations holder. A
065: * continuation is only valid for the same session it was created for. Session
066: * invalidation causes all bound continuations to be invalidated as well. Use
067: * this setting for web applications. Set "session-bound-continuations"
068: * configuration option to true to activate this mode.</li>
069: * </ul>
070: *
071: * @author <a href="mailto:ovidiu@cup.hp.com">Ovidiu Predescu </a>
072: * @author <a href="mailto:Michael.Melhem@managesoft.com">Michael Melhem </a>
073: * @since March 19, 2002
074: * @see ContinuationsManager
075: * @version CVS $Id: ContinuationsManagerImpl.java 433543 2006-08-22 06:22:54Z crossley $
076: */
077: public class ContinuationsManagerImpl extends AbstractLogEnabled
078: implements ContinuationsManager, Component, Configurable,
079: ThreadSafe, Instrumentable, Serviceable, Contextualizable {
080:
081: static final int CONTINUATION_ID_LENGTH = 20;
082: static final String EXPIRE_CONTINUATIONS = "expire-continuations";
083:
084: /**
085: * Random number generator used to create continuation ID
086: */
087: protected SecureRandom random;
088: protected byte[] bytes;
089:
090: /**
091: * How long does a continuation exist in memory since the last
092: * access? The time is in miliseconds, and the default is 1 hour.
093: */
094: protected int defaultTimeToLive;
095:
096: /**
097: * Maintains the forest of <code>WebContinuation</code> trees.
098: * This set is used only for debugging puroses by
099: * {@link #displayAllContinuations()} method.
100: */
101: protected Set forest = Collections.synchronizedSet(new HashSet());
102:
103: /**
104: * Main continuations holder. Used unless continuations are stored in user
105: * session.
106: */
107: protected WebContinuationsHolder continuationsHolder;
108:
109: /**
110: * Sorted set of <code>WebContinuation</code> instances, based on
111: * their expiration time. This is used by the background thread to
112: * invalidate continuations.
113: */
114: protected SortedSet expirations = Collections
115: .synchronizedSortedSet(new TreeSet());
116:
117: protected String instrumentableName;
118: protected ValueInstrument continuationsCount;
119: protected int continuationsCounter;
120: protected ValueInstrument forestSize;
121: protected ValueInstrument expirationsSize;
122: protected CounterInstrument continuationsCreated;
123: protected CounterInstrument continuationsInvalidated;
124: protected boolean isContinuationSharingBugCompatible;
125: protected boolean bindContinuationsToSession;
126:
127: protected ServiceManager serviceManager;
128: protected Context context;
129:
130: public ContinuationsManagerImpl() throws Exception {
131:
132: try {
133: random = SecureRandom.getInstance("SHA1PRNG");
134: } catch (java.security.NoSuchAlgorithmException nsae) {
135: // Maybe we are on IBM's SDK
136: random = SecureRandom.getInstance("IBMSecureRandom");
137: }
138: random.setSeed(System.currentTimeMillis());
139: bytes = new byte[CONTINUATION_ID_LENGTH];
140:
141: continuationsCount = new ValueInstrument("count");
142: continuationsCounter = 0;
143: forestSize = new ValueInstrument("forest-size");
144: expirationsSize = new ValueInstrument("expirations-size");
145: continuationsCreated = new CounterInstrument("creates");
146: continuationsInvalidated = new CounterInstrument("invalidates");
147: }
148:
149: /**
150: * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
151: */
152: public void service(final ServiceManager manager)
153: throws ServiceException {
154: this .serviceManager = manager;
155: }
156:
157: /**
158: * @see org.apache.avalon.framework.configuration.Configurable#configure(org.apache.avalon.framework.configuration.Configuration)
159: */
160: public void configure(Configuration config) {
161: this .defaultTimeToLive = config.getAttributeAsInteger(
162: "time-to-live", (3600 * 1000));
163: this .isContinuationSharingBugCompatible = config
164: .getAttributeAsBoolean(
165: "continuation-sharing-bug-compatible", false);
166: this .bindContinuationsToSession = config.getAttributeAsBoolean(
167: "session-bound-continuations", false);
168:
169: // create a global ContinuationsHolder if this the "session-bound-continuations" parameter is set to false
170: if (!this .bindContinuationsToSession) {
171: this .continuationsHolder = new WebContinuationsHolder();
172: }
173:
174: // create a thread that invalidates the continuations
175: final Configuration expireConf = config
176: .getChild("expirations-check");
177: final long initialDelay = expireConf.getChild("offset", true)
178: .getValueAsLong(180000);
179: final long interval = expireConf.getChild("period", true)
180: .getValueAsLong(180000);
181: try {
182: final RunnableManager runnableManager = (RunnableManager) serviceManager
183: .lookup(RunnableManager.ROLE);
184: runnableManager.execute(new Runnable() {
185: public void run() {
186: expireContinuations();
187: }
188: }, initialDelay, interval);
189: serviceManager.release(runnableManager);
190: } catch (Exception e) {
191: getLogger()
192: .warn(
193: "Could not enqueue continuations expiration task. "
194: + "Continuations will not automatically expire.",
195: e);
196: }
197: }
198:
199: /**
200: * @see org.apache.excalibur.instrument.Instrumentable#setInstrumentableName(java.lang.String)
201: */
202: public void setInstrumentableName(String instrumentableName) {
203: this .instrumentableName = instrumentableName;
204: }
205:
206: /**
207: * @see org.apache.excalibur.instrument.Instrumentable#getInstrumentableName()
208: */
209: public String getInstrumentableName() {
210: return instrumentableName;
211: }
212:
213: /**
214: * @see org.apache.excalibur.instrument.Instrumentable#getInstruments()
215: */
216: public Instrument[] getInstruments() {
217: return new Instrument[] { continuationsCount,
218: continuationsCreated, continuationsInvalidated,
219: forestSize };
220: }
221:
222: /**
223: * @see org.apache.excalibur.instrument.Instrumentable#getChildInstrumentables()
224: */
225: public Instrumentable[] getChildInstrumentables() {
226: return Instrumentable.EMPTY_INSTRUMENTABLE_ARRAY;
227: }
228:
229: /**
230: * @see org.apache.cocoon.components.flow.ContinuationsManager#createWebContinuation(java.lang.Object, org.apache.cocoon.components.flow.WebContinuation, int, java.lang.String, org.apache.cocoon.components.flow.ContinuationsDisposer)
231: */
232: public WebContinuation createWebContinuation(Object kont,
233: WebContinuation parent, int timeToLive,
234: String interpreterId, ContinuationsDisposer disposer) {
235: int ttl = (timeToLive == 0 ? defaultTimeToLive : timeToLive);
236:
237: WebContinuation wk = generateContinuation(kont, parent, ttl,
238: interpreterId, disposer);
239: wk.enableLogging(getLogger());
240:
241: if (parent == null) {
242: forest.add(wk);
243: forestSize.setValue(forest.size());
244: } else {
245: handleParentContinuationExpiration(parent);
246: }
247:
248: handleLeafContinuationExpiration(wk);
249:
250: if (getLogger().isDebugEnabled()) {
251: getLogger().debug("WK: Created continuation " + wk.getId());
252: }
253:
254: return wk;
255: }
256:
257: /**
258: * When a new continuation is created in @link #createWebContinuation(Object, WebContinuation, int, String, ContinuationsDisposer),
259: * it is registered in the expiration set in order to be evaluated by the invalidation mechanism.
260: */
261: protected void handleLeafContinuationExpiration(WebContinuation wk) {
262: expirations.add(wk);
263: expirationsSize.setValue(expirations.size());
264: }
265:
266: /**
267: * When a new continuation is created in @link #createWebContinuation(Object, WebContinuation, int, String, ContinuationsDisposer),
268: * its parent continuation is removed from the expiration set. This way only leaf continuations are part of
269: * the expiration set.
270: */
271: protected void handleParentContinuationExpiration(
272: WebContinuation parent) {
273: if (parent.getChildren().size() < 2) {
274: expirations.remove(parent);
275: }
276: }
277:
278: /**
279: * @see org.apache.cocoon.components.flow.ContinuationsManager#lookupWebContinuation(java.lang.String, java.lang.String)
280: */
281: public WebContinuation lookupWebContinuation(String id,
282: String interpreterId) {
283: // REVISIT: Is the following check needed to avoid threading issues:
284: // return wk only if !(wk.hasExpired) ?
285: WebContinuationsHolder continuationsHolder = lookupWebContinuationsHolder(false);
286: if (continuationsHolder == null) {
287: return null;
288: }
289:
290: WebContinuation kont = continuationsHolder.get(id);
291: if (kont != null) {
292: boolean interpreterMatches = kont
293: .interpreterMatches(interpreterId);
294: if (!interpreterMatches && getLogger().isWarnEnabled()) {
295: getLogger()
296: .warn(
297: "WK: Continuation ("
298: + kont.getId()
299: + ") lookup for wrong interpreter. Bound to: "
300: + kont.getInterpreterId()
301: + ", looked up for: "
302: + interpreterId);
303: }
304: return interpreterMatches
305: || isContinuationSharingBugCompatible ? kont : null;
306: }
307: return null;
308: }
309:
310: /**
311: * Create <code>WebContinuation</code> and generate unique identifier
312: * for it. The identifier is generated using a cryptographically strong
313: * algorithm to prevent people to generate their own identifiers.
314: *
315: * @param kont an <code>Object</code> value representing continuation
316: * @param parent value representing parent <code>WebContinuation</code>
317: * @param ttl <code>WebContinuation</code> time to live
318: * @param interpreterId id of interpreter invoking continuation creation
319: * @param disposer <code>ContinuationsDisposer</code> instance to use for
320: * cleanup of the continuation.
321: * @return the generated <code>WebContinuation</code> with unique identifier
322: */
323: protected WebContinuation generateContinuation(Object kont,
324: WebContinuation parent, int ttl, String interpreterId,
325: ContinuationsDisposer disposer) {
326:
327: char[] result = new char[bytes.length * 2];
328: WebContinuation wk = null;
329: WebContinuationsHolder continuationsHolder = lookupWebContinuationsHolder(true);
330: while (true) {
331: random.nextBytes(bytes);
332:
333: for (int i = 0; i < CONTINUATION_ID_LENGTH; i++) {
334: byte ch = bytes[i];
335: result[2 * i] = Character.forDigit(Math.abs(ch >> 4),
336: 16);
337: result[2 * i + 1] = Character.forDigit(Math
338: .abs(ch & 0x0f), 16);
339: }
340:
341: final String id = new String(result);
342: synchronized (continuationsHolder) {
343: if (!continuationsHolder.contains(id)) {
344: if (this .bindContinuationsToSession) {
345: wk = new HolderAwareWebContinuation(id, kont,
346: parent, ttl, interpreterId, disposer,
347: continuationsHolder);
348: } else {
349: wk = new WebContinuation(id, kont, parent, ttl,
350: interpreterId, disposer);
351: }
352: continuationsHolder.addContinuation(wk);
353: synchronized (continuationsCount) {
354: continuationsCounter++;
355: continuationsCount
356: .setValue(continuationsCounter);
357: }
358: break;
359: }
360: }
361: }
362:
363: continuationsCreated.increment();
364: return wk;
365: }
366:
367: /**
368: * @see org.apache.cocoon.components.flow.ContinuationsManager#invalidateWebContinuation(org.apache.cocoon.components.flow.WebContinuation)
369: */
370: public void invalidateWebContinuation(WebContinuation wk) {
371: WebContinuationsHolder continuationsHolder = lookupWebContinuationsHolder(false);
372: if (!continuationsHolder.contains(wk)) {
373: //TODO this looks like a security breach - should we throw?
374: return;
375: }
376: _detach(wk);
377: _invalidate(continuationsHolder, wk);
378: }
379:
380: protected void _invalidate(
381: WebContinuationsHolder continuationsHolder,
382: WebContinuation wk) {
383: if (getLogger().isDebugEnabled()) {
384: getLogger().debug(
385: "WK: Manual expire of continuation " + wk.getId());
386: }
387: disposeContinuation(continuationsHolder, wk);
388: expirations.remove(wk);
389: expirationsSize.setValue(expirations.size());
390:
391: // Invalidate all the children continuations as well
392: List children = wk.getChildren();
393: int size = children.size();
394: for (int i = 0; i < size; i++) {
395: _invalidate(continuationsHolder, (WebContinuation) children
396: .get(i));
397: }
398: }
399:
400: /**
401: * Detach this continuation from parent. This method removes
402: * continuation from {@link #forest} set, or, if it has parent,
403: * from parent's children collection.
404: * @param wk Continuation to detach from parent.
405: */
406: private void _detach(WebContinuation wk) {
407: WebContinuation parent = wk.getParentContinuation();
408: if (parent == null) {
409: forest.remove(wk);
410: forestSize.setValue(forest.size());
411: } else
412: wk.detachFromParent();
413: }
414:
415: /**
416: * Makes the continuation inaccessible for lookup, and triggers possible needed
417: * cleanup code through the ContinuationsDisposer interface.
418: * @param continuationsHolder
419: *
420: * @param wk the continuation to dispose.
421: */
422: protected void disposeContinuation(
423: WebContinuationsHolder continuationsHolder,
424: WebContinuation wk) {
425: continuationsHolder.removeContinuation(wk);
426: synchronized (continuationsCount) {
427: continuationsCounter--;
428: continuationsCount.setValue(continuationsCounter);
429: }
430: wk.dispose();
431: continuationsInvalidated.increment();
432: }
433:
434: /**
435: * Removes an expired leaf <code>WebContinuation</code> node
436: * from its continuation tree, and recursively removes its
437: * parent(s) if it they have expired and have no (other) children.
438: * @param continuationsHolder
439: *
440: * @param wk <code>WebContinuation</code> node
441: */
442: protected void removeContinuation(
443: WebContinuationsHolder continuationsHolder,
444: WebContinuation wk) {
445: if (wk.getChildren().size() != 0) {
446: return;
447: }
448:
449: // remove access to this contination
450: disposeContinuation(continuationsHolder, wk);
451: _detach(wk);
452:
453: if (getLogger().isDebugEnabled()) {
454: getLogger()
455: .debug("WK: Deleted continuation: " + wk.getId());
456: }
457:
458: // now check if parent needs to be removed.
459: WebContinuation parent = wk.getParentContinuation();
460: if (null != parent && parent.hasExpired()) {
461: //parent must have the same continuations holder, lookup not needed
462: removeContinuation(continuationsHolder, parent);
463: }
464: }
465:
466: /**
467: * Dump to Log file the current contents of
468: * the expirations <code>SortedSet</code>
469: */
470: protected void displayExpireSet() {
471: StringBuffer wkSet = new StringBuffer("\nWK; Expire set size: "
472: + expirations.size());
473: Iterator i = expirations.iterator();
474: while (i.hasNext()) {
475: final WebContinuation wk = (WebContinuation) i.next();
476: final long lat = wk.getLastAccessTime()
477: + wk.getTimeToLive();
478: wkSet.append("\nWK: ").append(wk.getId()).append(
479: " ExpireTime [");
480:
481: if (lat < System.currentTimeMillis()) {
482: wkSet.append("Expired");
483: } else {
484: wkSet.append(lat);
485: }
486: wkSet.append("]");
487: }
488:
489: getLogger().debug(wkSet.toString());
490: }
491:
492: /**
493: * Dump to Log file all <code>WebContinuation</code>s
494: * in the system
495: */
496: public void displayAllContinuations() {
497: final Iterator i = forest.iterator();
498: while (i.hasNext()) {
499: ((WebContinuation) i.next()).display();
500: }
501: }
502:
503: /**
504: * Remove all continuations which have already expired.
505: */
506: protected void expireContinuations() {
507: long now = 0;
508: if (getLogger().isDebugEnabled()) {
509: now = System.currentTimeMillis();
510:
511: /* Continuations before clean up: */
512: getLogger().debug(
513: "WK: Forest before cleanup: " + forest.size());
514: displayAllContinuations();
515: displayExpireSet();
516:
517: }
518:
519: // Clean up expired continuations
520: int count = 0;
521: WebContinuation wk;
522: Iterator i = expirations.iterator();
523: while (i.hasNext()
524: && ((wk = (WebContinuation) i.next()).hasExpired())) {
525: i.remove();
526: WebContinuationsHolder continuationsHolder = null;
527: if (wk instanceof HolderAwareWebContinuation) {
528: continuationsHolder = ((HolderAwareWebContinuation) wk)
529: .getContinuationsHolder();
530: } else {
531: continuationsHolder = this .continuationsHolder;
532: }
533: removeContinuation(continuationsHolder, wk);
534: count++;
535: }
536: expirationsSize.setValue(expirations.size());
537:
538: if (getLogger().isDebugEnabled()) {
539: getLogger().debug(
540: "WK Cleaned up " + count + " continuations in "
541: + (System.currentTimeMillis() - now)
542: + " ms");
543:
544: /* Continuations after clean up: */
545: // getLogger().debug("WK: Forest after cleanup: " + forest.size());
546: // displayAllContinuations();
547: // displayExpireSet();
548: }
549: }
550:
551: /**
552: * Method used by WebContinuationsHolder to notify the continuations manager
553: * about session invalidation. Invalidates all continuations held by passed
554: * continuationsHolder.
555: */
556: protected void invalidateContinuations(
557: WebContinuationsHolder continuationsHolder) {
558: // TODO: this avoids ConcurrentModificationException, still this is not
559: // the best solution and should be changed
560: Object[] continuationIds = continuationsHolder
561: .getContinuationIds().toArray();
562:
563: for (int i = 0; i < continuationIds.length; i++) {
564: WebContinuation wk = continuationsHolder
565: .get(continuationIds[i]);
566: if (wk != null) {
567: _detach(wk);
568: _invalidate(continuationsHolder, wk);
569: }
570: }
571: }
572:
573: /**
574: * Lookup a proper web continuations holder.
575: * @param createNew
576: * should the manager create a continuations holder in session
577: * when none found?
578: */
579: public WebContinuationsHolder lookupWebContinuationsHolder(
580: boolean createNew) {
581: //there is only one holder if continuations are not bound to session
582: if (!this .bindContinuationsToSession)
583: return this .continuationsHolder;
584:
585: //if continuations bound to session lookup a proper holder in the session
586: Map objectModel = ContextHelper.getObjectModel(this .context);
587: Request request = ObjectModelHelper.getRequest(objectModel);
588:
589: if (!createNew && request.getSession(false) == null)
590: return null;
591:
592: Session session = request.getSession(true);
593: WebContinuationsHolder holder = (WebContinuationsHolder) session
594: .getAttribute(WebContinuationsHolder.CONTINUATIONS_HOLDER);
595: if (!createNew)
596: return holder;
597:
598: if (holder != null)
599: return holder;
600:
601: holder = new WebContinuationsHolder();
602: session.setAttribute(
603: WebContinuationsHolder.CONTINUATIONS_HOLDER, holder);
604: return holder;
605: }
606:
607: /**
608: * A holder for WebContinuations. When bound to session notifies the
609: * continuations manager of session invalidation.
610: */
611: protected class WebContinuationsHolder implements
612: HttpSessionBindingListener {
613: private final static String CONTINUATIONS_HOLDER = "o.a.c.c.f.SCMI.WebContinuationsHolder";
614:
615: private Map holder = Collections.synchronizedMap(new HashMap());
616:
617: public WebContinuation get(Object id) {
618: return (WebContinuation) this .holder.get(id);
619: }
620:
621: public void addContinuation(WebContinuation wk) {
622: this .holder.put(wk.getId(), wk);
623: }
624:
625: public void removeContinuation(WebContinuation wk) {
626: this .holder.remove(wk.getId());
627: }
628:
629: public Set getContinuationIds() {
630: return holder.keySet();
631: }
632:
633: public boolean contains(String continuationId) {
634: return this .holder.containsKey(continuationId);
635: }
636:
637: public boolean contains(WebContinuation wk) {
638: return contains(wk.getId());
639: }
640:
641: public void valueBound(HttpSessionBindingEvent event) {
642: }
643:
644: public void valueUnbound(HttpSessionBindingEvent event) {
645: invalidateContinuations(this );
646: }
647: }
648:
649: /**
650: * WebContinuation extension that holds also the information about the
651: * holder. This information is needed to cleanup a proper holder after
652: * continuation's expiration time.
653: */
654: protected static class HolderAwareWebContinuation extends
655: WebContinuation {
656: private WebContinuationsHolder continuationsHolder;
657:
658: public HolderAwareWebContinuation(String id,
659: Object continuation,
660: WebContinuation parentContinuation, int timeToLive,
661: String interpreterId, ContinuationsDisposer disposer,
662: WebContinuationsHolder continuationsHolder) {
663: super (id, continuation, parentContinuation, timeToLive,
664: interpreterId, disposer);
665: this .continuationsHolder = continuationsHolder;
666: }
667:
668: public WebContinuationsHolder getContinuationsHolder() {
669: return continuationsHolder;
670: }
671:
672: //retain comparation logic from parent
673: public int compareTo(Object other) {
674: return super .compareTo(other);
675: }
676: }
677:
678: /**
679: * @see org.apache.avalon.framework.context.Contextualizable#contextualize(org.apache.avalon.framework.context.Context)
680: */
681: public void contextualize(Context context) throws ContextException {
682: this .context = context;
683: }
684:
685: /**
686: * Get a list of all web continuations (data only)
687: */
688: public List getWebContinuationsDataBeanList() {
689: List beanList = new ArrayList();
690: for (Iterator it = this .forest.iterator(); it.hasNext();) {
691: beanList.add(new WebContinuationDataBean(
692: (WebContinuation) it.next()));
693: }
694: return beanList;
695: }
696:
697: }
|