View Javadoc

1   /*
2    * Copyright 1999-2004 The Apache Software Foundation.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.apache.struts.flow.core;
17  
18  import java.security.SecureRandom;
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.SortedSet;
28  import java.util.TreeSet;
29  
30  import javax.servlet.http.HttpSessionBindingEvent;
31  import javax.servlet.http.HttpSessionBindingListener;
32  
33  import org.apache.commons.chain.web.WebContext;
34  import org.apache.commons.chain.web.servlet.ServletWebContext;
35  
36  //import org.apache.avalon.framework.configuration.Configurable;
37  //import org.apache.avalon.framework.configuration.Configuration;
38  //import org.apache.avalon.framework.context.Context;
39  //import org.apache.avalon.framework.context.ContextException;
40  //import org.apache.avalon.framework.context.Contextualizable;
41  //import org.apache.avalon.framework.logger.AbstractLogEnabled;
42  //import org.apache.avalon.framework.service.ServiceException;
43  //import org.apache.avalon.framework.service.ServiceManager;
44  //import org.apache.avalon.framework.service.Serviceable;
45  //import org.apache.avalon.framework.thread.ThreadSafe;
46  //import org.apache.cocoon.components.ContextHelper;
47  //import org.apache.cocoon.components.thread.RunnableManager;
48  //import org.apache.cocoon.environment.ObjectModelHelper;
49  //import org.apache.cocoon.environment.Request;
50  //import org.apache.cocoon.environment.Session;
51  
52  
53  /***
54   * The default implementation of {@link ContinuationsManager}. <br/>There are
55   * two modes of work: <br/>
56   * <ul>
57   * <li><b>standard mode </b>- continuations are stored in single holder. No
58   * security is applied to continuation lookup. Anyone can invoke a continuation
59   * only knowing the ID. Set "session-bound-continuations" configuration option
60   * to false to activate this mode.</li>
61   * <li><b>secure mode </b>- each session has it's own continuations holder. A
62   * continuation is only valid for the same session it was created for. Session
63   * invalidation causes all bound continuations to be invalidated as well. Use
64   * this setting for web applications. Set "session-bound-continuations"
65   * configuration option to true to activate this mode.</li>
66   * </ul>
67   * 
68   * @author <a href="mailto:ovidiu@cup.hp.com">Ovidiu Predescu </a>
69   * @author <a href="mailto:Michael.Melhem@managesoft.com">Michael Melhem </a>
70   * @since March 19, 2002
71   * @see ContinuationsManager
72   * @version CVS $Id: ContinuationsManagerImpl.java 293111 2005-10-02 13:39:20Z reinhard $
73   */
74  public class ContinuationsManagerImpl implements ContinuationsManager  {
75  
76      static final int CONTINUATION_ID_LENGTH = 20;
77      static final String EXPIRE_CONTINUATIONS = "expire-continuations";
78  
79      /***
80       * Random number generator used to create continuation ID
81       */
82      protected SecureRandom random;
83      protected byte[] bytes;
84  
85      /***
86       * How long does a continuation exist in memory since the last
87       * access? The time is in miliseconds, and the default is 1 hour.
88       */
89      protected int defaultTimeToLive = 3600 * 1000;
90  
91      /***
92       * Maintains the forest of <code>WebContinuation</code> trees.
93       * This set is used only for debugging puroses by
94       * {@link #displayAllContinuations()} method.
95       */
96      protected Set forest = Collections.synchronizedSet(new HashSet());
97  
98      /***
99       * Main continuations holder. Used unless continuations are stored in user
100      * session.
101      */
102     protected WebContinuationsHolder continuationsHolder;
103     
104     /***
105      * Sorted set of <code>WebContinuation</code> instances, based on
106      * their expiration time. This is used by the background thread to
107      * invalidate continuations.
108      */
109     protected SortedSet expirations = Collections.synchronizedSortedSet(new TreeSet());
110 
111     protected boolean bindContinuationsToSession;
112 
113     private Thread expireThread;
114     
115     private long expirePeriod = 180000;
116 
117     public ContinuationsManagerImpl() throws Exception {
118         try {
119             random = SecureRandom.getInstance("SHA1PRNG");
120         } catch(java.security.NoSuchAlgorithmException nsae) {
121             // Maybe we are on IBM's SDK
122             random = SecureRandom.getInstance("IBMSecureRandom");
123         }
124         random.setSeed(System.currentTimeMillis());
125         bytes = new byte[CONTINUATION_ID_LENGTH];
126         expireThread = new Thread(
127             new Runnable() {
128                 public void run() {
129                     boolean shouldKeepRunning = true;
130                     while (shouldKeepRunning) {
131                         try {
132                             Thread.sleep(expirePeriod);
133                         } catch (InterruptedException ex) {
134                             getLogger().debug("Continuation expiration thread interrupted");
135                             shouldKeepRunning = false;
136                         }
137                         if (shouldKeepRunning) {
138                             expireContinuations();
139                         }
140                     }
141                 }
142             });
143 
144         getLogger().debug("Starting continuation expiration thread");
145         expireThread.setName("Flow continuations expiration thread");
146         expireThread.setPriority(Thread.MIN_PRIORITY);
147         expireThread.start();
148         
149         this.continuationsHolder = new WebContinuationsHolder();
150     }
151     
152     
153     /***
154      *  Gets the logger 
155      *
156      *@return    The logger value
157      */
158     public Logger getLogger() {
159         return Factory.getLogger();
160     }
161     
162     
163     /***
164      *  Set the default time to live value
165      *
166      *@param  ttl  The time-to-live in milliseconds
167      */
168     public void setDefaultTimeToLive(int ttl) {
169         this.defaultTimeToLive = ttl;
170     }
171     
172     public void setExpirationPeriod(long period) {
173         this.expirePeriod = period;
174     }
175     
176     public void setBindContinuationsToSession(boolean bind) {
177         this.bindContinuationsToSession = bind;
178         if (this.bindContinuationsToSession) {
179             this.continuationsHolder = null;
180         }
181     }
182 
183 
184     public WebContinuation createWebContinuation(Object kont,
185                                                  WebContinuation parent,
186                                                  int timeToLive,
187                                                  String interpreterId, 
188                                                  ContinuationsDisposer disposer,
189                                                  WebContext webctx) {
190         int ttl = (timeToLive == 0 ? defaultTimeToLive : timeToLive);
191 
192         WebContinuation wk = generateContinuation(kont, parent, ttl, interpreterId, disposer, webctx);
193         //wk.enableLogging(getLogger());
194 
195         if (parent == null) {
196             forest.add(wk);
197         } else {
198             handleParentContinuationExpiration(parent);
199         }
200 
201         handleLeafContinuationExpiration(wk);
202 
203         if (getLogger().isDebugEnabled()) {
204             getLogger().debug("WK: Created continuation " + wk.getId());
205         }
206 
207         return wk;
208     }
209     
210     /***
211      * When a new continuation is created in @link #createWebContinuation(Object, WebContinuation, int, String, ContinuationsDisposer),
212      * it is registered in the expiration set in order to be evaluated by the invalidation mechanism.
213      */
214     protected void handleLeafContinuationExpiration(WebContinuation wk) {
215         expirations.add(wk);
216     }
217 
218     /***
219      * When a new continuation is created in @link #createWebContinuation(Object, WebContinuation, int, String, ContinuationsDisposer),
220      * its parent continuation is removed from the expiration set. This way only leaf continuations are part of
221      * the expiration set.
222      */
223     protected void handleParentContinuationExpiration(WebContinuation parent) {
224         if (parent.getChildren().size() < 2) {
225             expirations.remove(parent);
226         }
227     }    
228     
229     /***
230      * Get a list of all web continuations (data only)
231      */
232     public List getWebContinuationsDataBeanList() {
233         List beanList = new ArrayList();
234         for(Iterator it = this.forest.iterator(); it.hasNext();) {
235             beanList.add(new WebContinuationDataBean((WebContinuation) it.next()));
236         }
237         return beanList;
238     }
239 
240     public WebContinuation lookupWebContinuation(String id, String interpreterId, WebContext webctx) {
241         // REVISIT: Is the following check needed to avoid threading issues:
242         // return wk only if !(wk.hasExpired) ?
243         WebContinuationsHolder continuationsHolder = lookupWebContinuationsHolder(false, webctx);
244         if (continuationsHolder == null)
245             return null;
246         
247         WebContinuation kont = continuationsHolder.get(id);
248         if (kont == null)
249             return null;
250             
251         if (!kont.interpreterMatches(interpreterId)) {
252             getLogger().error(
253                     "WK: Continuation (" + kont.getId()
254                             + ") lookup for wrong interpreter. Bound to: "
255                             + kont.getInterpreterId() + ", looked up for: "
256                             + interpreterId);
257             return null;
258         }
259         return kont;
260     }
261 
262     /***
263      * Create <code>WebContinuation</code> and generate unique identifier for
264      * it. The identifier is generated using a cryptographically strong
265      * algorithm to prevent people to generate their own identifiers.
266      * 
267      * <p>
268      * It has the side effect of interning the continuation object in the
269      * <code>idToWebCont</code> hash table.
270      * 
271      * @param kont
272      *            an <code>Object</code> value representing continuation
273      * @param parent
274      *            value representing parent <code>WebContinuation</code>
275      * @param ttl
276      *            <code>WebContinuation</code> time to live
277      * @param interpreterId
278      *            id of interpreter invoking continuation creation
279      * @param disposer
280      *            <code>ContinuationsDisposer</code> instance to use for
281      *            cleanup of the continuation.
282      * @return the generated <code>WebContinuation</code> with unique
283      *         identifier
284      */
285     protected WebContinuation generateContinuation(Object kont,
286                                                  WebContinuation parent,
287                                                  int ttl,
288                                                  String interpreterId,
289                                                  ContinuationsDisposer disposer,
290                                                  WebContext webctx) {
291 
292         char[] result = new char[bytes.length * 2];
293         WebContinuation wk = null;
294         WebContinuationsHolder continuationsHolder = lookupWebContinuationsHolder(true, webctx);
295         while (true) {
296             random.nextBytes(bytes);
297 
298             for (int i = 0; i < CONTINUATION_ID_LENGTH; i++) {
299                 byte ch = bytes[i];
300                 result[2 * i] = Character.forDigit(Math.abs(ch >> 4), 16);
301                 result[2 * i + 1] = Character.forDigit(Math.abs(ch & 0x0f), 16);
302             }
303 
304             final String id = new String(result);
305             synchronized (continuationsHolder) {
306                 if (!continuationsHolder.contains(id)) {
307                     if (this.bindContinuationsToSession)
308                         wk = new HolderAwareWebContinuation(id, kont, parent,
309                                 ttl, interpreterId, disposer,
310                                 continuationsHolder);
311                     else
312                         wk = new WebContinuation(id, kont, parent, ttl,
313                                 interpreterId, disposer);
314                     continuationsHolder.addContinuation(wk);
315                     break;
316                 }
317             }
318         }
319 
320         return wk;
321     }
322 
323     public void invalidateWebContinuation(WebContinuation wk, WebContext webctx) {
324         WebContinuationsHolder continuationsHolder = lookupWebContinuationsHolder(false, webctx);
325         if (!continuationsHolder.contains(wk)) {
326             //TODO this looks like a security breach - should we throw?
327             return;
328         }
329         _detach(wk);
330         _invalidate(continuationsHolder, wk);
331     }
332 
333     private void _invalidate(WebContinuationsHolder continuationsHolder, WebContinuation wk) {
334         if (getLogger().isDebugEnabled()) {
335             getLogger().debug("WK: Manual expire of continuation " + wk.getId());
336         }
337         disposeContinuation(continuationsHolder, wk);
338         expirations.remove(wk);
339 
340         // Invalidate all the children continuations as well
341         List children = wk.getChildren();
342         int size = children.size();
343         for (int i = 0; i < size; i++) {
344             _invalidate(continuationsHolder, (WebContinuation) children.get(i));
345         }
346     }
347 
348     /***
349      * Detach this continuation from parent. This method removes
350      * continuation from {@link #forest} set, or, if it has parent,
351      * from parent's children collection.
352      * @param continuationsHolder
353      * @param wk Continuation to detach from parent.
354      */
355     protected void _detach(WebContinuation wk) {
356         WebContinuation parent = wk.getParentContinuation();
357         if (parent == null) {
358             forest.remove(wk);
359         } else 
360             wk.detachFromParent();
361     }
362 
363     /***
364      * Makes the continuation inaccessible for lookup, and triggers possible needed
365      * cleanup code through the ContinuationsDisposer interface.
366      * @param continuationsHolder
367      *
368      * @param wk the continuation to dispose.
369      */
370     protected void disposeContinuation(WebContinuationsHolder continuationsHolder, WebContinuation wk) {
371         continuationsHolder.removeContinuation(wk);
372         wk.dispose();
373     }
374 
375     /***
376      * Removes an expired leaf <code>WebContinuation</code> node
377      * from its continuation tree, and recursively removes its
378      * parent(s) if it they have expired and have no (other) children.
379      * @param continuationsHolder
380      *
381      * @param wk <code>WebContinuation</code> node
382      */
383     protected void removeContinuation(WebContinuationsHolder continuationsHolder,
384             WebContinuation wk) {
385         if (wk.getChildren().size() != 0) {
386             return;
387         }
388 
389         // remove access to this contination
390         disposeContinuation(continuationsHolder, wk);
391         _detach(wk);
392 
393         if (getLogger().isDebugEnabled()) {
394             getLogger().debug("WK: Deleted continuation: " + wk.getId());
395         }
396 
397         // now check if parent needs to be removed.
398         WebContinuation parent = wk.getParentContinuation();
399         if (null != parent && parent.hasExpired()) {
400             //parent must have the same continuations holder, lookup not needed
401             removeContinuation(continuationsHolder, parent);
402         }
403     }
404 
405     /***
406      * Dump to Log file the current contents of
407      * the expirations <code>SortedSet</code>
408      */
409     protected void displayExpireSet() {
410         StringBuffer wkSet = new StringBuffer("\nWK; Expire set size: " + expirations.size());
411         Iterator i = expirations.iterator();
412         while (i.hasNext()) {
413             final WebContinuation wk = (WebContinuation) i.next();
414             final long lat = wk.getLastAccessTime() + wk.getTimeToLive();
415             wkSet.append("\nWK: ")
416                     .append(wk.getId())
417                     .append(" ExpireTime [");
418 
419             if (lat < System.currentTimeMillis()) {
420                 wkSet.append("Expired");
421             } else {
422                 wkSet.append(lat);
423             }
424             wkSet.append("]");
425         }
426 
427         getLogger().debug(wkSet.toString());
428     }
429 
430     /***
431      * Dump to Log file all <code>WebContinuation</code>s
432      * in the system
433      */
434     public void displayAllContinuations() {
435         final Iterator i = forest.iterator();
436         while (i.hasNext()) {
437             ((WebContinuation) i.next()).display();
438         }
439     }
440 
441     /***
442      * Remove all continuations which have already expired.
443      */
444     protected void expireContinuations() {
445         long now = 0;
446         if (getLogger().isDebugEnabled()) {
447             now = System.currentTimeMillis();
448 
449             /* Continuations before clean up:
450             getLogger().debug("WK: Forest before cleanup: " + forest.size());
451             displayAllContinuations();
452             displayExpireSet();
453             */
454         }
455 
456         // Clean up expired continuations
457         int count = 0;
458         WebContinuation wk;
459         Iterator i = expirations.iterator();
460         while (i.hasNext() && ((wk = (WebContinuation) i.next()).hasExpired())) {
461             i.remove();
462             WebContinuationsHolder continuationsHolder = null;
463             if ( wk instanceof HolderAwareWebContinuation )
464                 continuationsHolder = ((HolderAwareWebContinuation) wk).getContinuationsHolder();
465             else
466                 continuationsHolder = this.continuationsHolder;
467             removeContinuation(continuationsHolder, wk);
468             count++;
469         }
470 
471         if (getLogger().isDebugEnabled()) {
472             getLogger().debug("WK Cleaned up " + count + " continuations in " +
473                               (System.currentTimeMillis() - now));
474 
475             /* Continuations after clean up:
476             getLogger().debug("WK: Forest after cleanup: " + forest.size());
477             displayAllContinuations();
478             displayExpireSet();
479             */
480         }
481     }
482 
483     /***
484      * Method used by WebContinuationsHolder to notify the continuations manager
485      * about session invalidation. Invalidates all continuations held by passed
486      * continuationsHolder.
487      */
488     protected void invalidateContinuations(
489             WebContinuationsHolder continuationsHolder) {
490         // TODO: this avoids ConcurrentModificationException, still this is not
491         // the best solution and should be changed
492         Object[] continuationIds = continuationsHolder.getContinuationIds()
493                 .toArray();
494         
495         for (int i = 0; i < continuationIds.length; i++) {
496             WebContinuation wk = continuationsHolder.get(continuationIds[i]);
497             if (wk != null) {
498                 _detach(wk);
499                 _invalidate(continuationsHolder, wk);
500             }
501         }
502     }
503 
504     /***
505      * Lookup a proper web continuations holder. 
506      * @param createNew
507      *            should the manager create a continuations holder in session
508      *            when none found?
509      */
510     public WebContinuationsHolder lookupWebContinuationsHolder(boolean createNew, WebContext webctx) {
511         //there is only one holder if continuations are not bound to session
512         if (!this.bindContinuationsToSession)
513             return this.continuationsHolder;
514         
515         //if continuations bound to session lookup a proper holder in the session
516         if (!createNew && webctx instanceof ServletWebContext) {
517             if (((ServletWebContext) webctx).getRequest().getSession(false) == null) {
518                 return null;
519             }
520         }
521 
522         WebContinuationsHolder holder = 
523             (WebContinuationsHolder) webctx.getSessionScope().get(
524                     WebContinuationsHolder.CONTINUATIONS_HOLDER);
525         if (!createNew)
526             return holder;
527 
528         if (holder != null)
529             return holder;
530 
531         holder = new WebContinuationsHolder();
532         webctx.getSessionScope().put(WebContinuationsHolder.CONTINUATIONS_HOLDER,
533                 holder);
534         return holder;
535     }
536 
537     /***
538      * A holder for WebContinuations. When bound to session notifies the
539      * continuations manager of session invalidation.
540      */
541     public class WebContinuationsHolder implements HttpSessionBindingListener {
542         private final static String CONTINUATIONS_HOLDER = 
543                                        "o.a.c.c.f.SCMI.WebContinuationsHolder";
544 
545         private Map holder = Collections.synchronizedMap(new HashMap());
546 
547         public WebContinuation get(Object id) {
548             return (WebContinuation) this.holder.get(id);
549         }
550 
551         public void addContinuation(WebContinuation wk) {
552             this.holder.put(wk.getId(), wk);
553         }
554 
555         public void removeContinuation(WebContinuation wk) {
556             this.holder.remove(wk.getId());
557         }
558 
559         public Set getContinuationIds() {
560             return holder.keySet();
561         }
562         
563         public boolean contains(String continuationId) {
564             return this.holder.containsKey(continuationId);
565         }
566         
567         public boolean contains(WebContinuation wk) {
568             return contains(wk.getId());
569         }
570 
571         public void valueBound(HttpSessionBindingEvent event) {
572         }
573 
574         public void valueUnbound(HttpSessionBindingEvent event) {
575             invalidateContinuations(this);
576         }
577     }
578 
579     /***
580      * WebContinuation extension that holds also the information about the
581      * holder. This information is needed to cleanup a proper holder after
582      * continuation's expiration time.
583      */
584     protected class HolderAwareWebContinuation extends WebContinuation {
585         private WebContinuationsHolder continuationsHolder;
586 
587         public HolderAwareWebContinuation(String id, Object continuation,
588                 WebContinuation parentContinuation, int timeToLive,
589                 String interpreterId, ContinuationsDisposer disposer,
590                 WebContinuationsHolder continuationsHolder) {
591             super(id, continuation, parentContinuation, timeToLive,
592                     interpreterId, disposer);
593             this.continuationsHolder = continuationsHolder;
594         }
595 
596         public WebContinuationsHolder getContinuationsHolder() {
597             return continuationsHolder;
598         }
599 
600         //retain comparation logic from parent
601         public int compareTo(Object other) {
602             return super.compareTo(other);
603         }
604     }
605     
606     
607     /***  Destroys all continuations and any other resident objects  */
608     public void destroy() {
609         /*expirations.clear();
610         Set clone = new HashSet(forest);
611         for (Iterator i = clone.iterator(); i.hasNext(); ) {
612             removeContinuation((WebContinuation) i.next());
613         }
614         */
615         if (expireThread != null && expireThread.isAlive()) {
616             expireThread.interrupt();
617         }
618     }
619 
620 }