1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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
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
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
242
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
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
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
390 disposeContinuation(continuationsHolder, wk);
391 _detach(wk);
392
393 if (getLogger().isDebugEnabled()) {
394 getLogger().debug("WK: Deleted continuation: " + wk.getId());
395 }
396
397
398 WebContinuation parent = wk.getParentContinuation();
399 if (null != parent && parent.hasExpired()) {
400
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
450
451
452
453
454 }
455
456
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
476
477
478
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
491
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
512 if (!this.bindContinuationsToSession)
513 return this.continuationsHolder;
514
515
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
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
610
611
612
613
614
615 if (expireThread != null && expireThread.isAlive()) {
616 expireThread.interrupt();
617 }
618 }
619
620 }