001: /*
002: * $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/session/StandardManager.java,v 1.19 2002/06/09 02:19:43 remm Exp $
003: * $Revision: 1.19 $
004: * $Date: 2002/06/09 02:19:43 $
005: *
006: * ====================================================================
007: *
008: * The Apache Software License, Version 1.1
009: *
010: * Copyright (c) 1999 The Apache Software Foundation. All rights
011: * reserved.
012: *
013: * Redistribution and use in source and binary forms, with or without
014: * modification, are permitted provided that the following conditions
015: * are met:
016: *
017: * 1. Redistributions of source code must retain the above copyright
018: * notice, this list of conditions and the following disclaimer.
019: *
020: * 2. Redistributions in binary form must reproduce the above copyright
021: * notice, this list of conditions and the following disclaimer in
022: * the documentation and/or other materials provided with the
023: * distribution.
024: *
025: * 3. The end-user documentation included with the redistribution, if
026: * any, must include the following acknowlegement:
027: * "This product includes software developed by the
028: * Apache Software Foundation (http://www.apache.org/)."
029: * Alternately, this acknowlegement may appear in the software itself,
030: * if and wherever such third-party acknowlegements normally appear.
031: *
032: * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
033: * Foundation" must not be used to endorse or promote products derived
034: * from this software without prior written permission. For written
035: * permission, please contact apache@apache.org.
036: *
037: * 5. Products derived from this software may not be called "Apache"
038: * nor may "Apache" appear in their names without prior written
039: * permission of the Apache Group.
040: *
041: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
042: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
043: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
044: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
045: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
046: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
047: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
048: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
049: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
050: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
051: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
052: * SUCH DAMAGE.
053: * ====================================================================
054: *
055: * This software consists of voluntary contributions made by many
056: * individuals on behalf of the Apache Software Foundation. For more
057: * information on the Apache Software Foundation, please see
058: * <http://www.apache.org/>.
059: *
060: * [Additional notices, if required by prior licensing conditions]
061: *
062: */
063:
064: package org.apache.catalina.session;
065:
066: import java.beans.PropertyChangeEvent;
067: import java.beans.PropertyChangeListener;
068: import java.beans.PropertyChangeSupport;
069: import java.io.BufferedInputStream;
070: import java.io.BufferedOutputStream;
071: import java.io.File;
072: import java.io.FileInputStream;
073: import java.io.FileNotFoundException;
074: import java.io.FileOutputStream;
075: import java.io.InputStream;
076: import java.io.IOException;
077: import java.io.ObjectInputStream;
078: import java.io.ObjectOutputStream;
079: import java.io.ObjectStreamClass;
080: import java.util.ArrayList;
081: import java.util.Iterator;
082: import javax.servlet.ServletContext;
083: import org.apache.catalina.Container;
084: import org.apache.catalina.Context;
085: import org.apache.catalina.Globals;
086: import org.apache.catalina.Lifecycle;
087: import org.apache.catalina.LifecycleEvent;
088: import org.apache.catalina.LifecycleException;
089: import org.apache.catalina.LifecycleListener;
090: import org.apache.catalina.Loader;
091: import org.apache.catalina.Manager;
092: import org.apache.catalina.Session;
093: import org.apache.catalina.util.CustomObjectInputStream;
094: import org.apache.catalina.util.LifecycleSupport;
095:
096: /**
097: * Standard implementation of the <b>Manager</b> interface that provides
098: * simple session persistence across restarts of this component (such as
099: * when the entire server is shut down and restarted, or when a particular
100: * web application is reloaded.
101: * <p>
102: * <b>IMPLEMENTATION NOTE</b>: Correct behavior of session storing and
103: * reloading depends upon external calls to the <code>start()</code> and
104: * <code>stop()</code> methods of this class at the correct times.
105: *
106: * @author Craig R. McClanahan
107: * @version $Revision: 1.19 $ $Date: 2002/06/09 02:19:43 $
108: */
109:
110: public class StandardManager extends ManagerBase implements Lifecycle,
111: PropertyChangeListener, Runnable {
112:
113: // ----------------------------------------------------- Instance Variables
114:
115: /**
116: * The interval (in seconds) between checks for expired sessions.
117: */
118: private int checkInterval = 60;
119:
120: /**
121: * The descriptive information about this implementation.
122: */
123: private static final String info = "StandardManager/1.0";
124:
125: /**
126: * The lifecycle event support for this component.
127: */
128: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
129:
130: /**
131: * The maximum number of active Sessions allowed, or -1 for no limit.
132: */
133: private int maxActiveSessions = -1;
134:
135: /**
136: * The descriptive name of this Manager implementation (for logging).
137: */
138: protected static String name = "StandardManager";
139:
140: /**
141: * Path name of the disk file in which active sessions are saved
142: * when we stop, and from which these sessions are loaded when we start.
143: * A <code>null</code> value indicates that no persistence is desired.
144: * If this pathname is relative, it will be resolved against the
145: * temporary working directory provided by our context, available via
146: * the <code>javax.servlet.context.tempdir</code> context attribute.
147: */
148: private String pathname = "SESSIONS.ser";
149:
150: /**
151: * Has this component been started yet?
152: */
153: private boolean started = false;
154:
155: /**
156: * The background thread.
157: */
158: private Thread thread = null;
159:
160: /**
161: * The background thread completion semaphore.
162: */
163: private boolean threadDone = false;
164:
165: /**
166: * Name to register for the background thread.
167: */
168: private String threadName = "StandardManager";
169:
170: // ------------------------------------------------------------- Properties
171:
172: /**
173: * Return the check interval (in seconds) for this Manager.
174: */
175: public int getCheckInterval() {
176:
177: return (this .checkInterval);
178:
179: }
180:
181: /**
182: * Set the check interval (in seconds) for this Manager.
183: *
184: * @param checkInterval The new check interval
185: */
186: public void setCheckInterval(int checkInterval) {
187:
188: int oldCheckInterval = this .checkInterval;
189: this .checkInterval = checkInterval;
190: support.firePropertyChange("checkInterval", new Integer(
191: oldCheckInterval), new Integer(this .checkInterval));
192:
193: }
194:
195: /**
196: * Set the Container with which this Manager has been associated. If
197: * it is a Context (the usual case), listen for changes to the session
198: * timeout property.
199: *
200: * @param container The associated Container
201: */
202: public void setContainer(Container container) {
203:
204: // De-register from the old Container (if any)
205: if ((this .container != null)
206: && (this .container instanceof Context))
207: ((Context) this .container)
208: .removePropertyChangeListener(this );
209:
210: // Default processing provided by our superclass
211: super .setContainer(container);
212:
213: // Register with the new Container (if any)
214: if ((this .container != null)
215: && (this .container instanceof Context)) {
216: setMaxInactiveInterval(((Context) this .container)
217: .getSessionTimeout() * 60);
218: ((Context) this .container).addPropertyChangeListener(this );
219: }
220:
221: }
222:
223: /**
224: * Return descriptive information about this Manager implementation and
225: * the corresponding version number, in the format
226: * <code><description>/<version></code>.
227: */
228: public String getInfo() {
229:
230: return (this .info);
231:
232: }
233:
234: /**
235: * Return the maximum number of active Sessions allowed, or -1 for
236: * no limit.
237: */
238: public int getMaxActiveSessions() {
239:
240: return (this .maxActiveSessions);
241:
242: }
243:
244: /**
245: * Set the maximum number of actives Sessions allowed, or -1 for
246: * no limit.
247: *
248: * @param max The new maximum number of sessions
249: */
250: public void setMaxActiveSessions(int max) {
251:
252: int oldMaxActiveSessions = this .maxActiveSessions;
253: this .maxActiveSessions = max;
254: support.firePropertyChange("maxActiveSessions", new Integer(
255: oldMaxActiveSessions), new Integer(
256: this .maxActiveSessions));
257:
258: }
259:
260: /**
261: * Return the descriptive short name of this Manager implementation.
262: */
263: public String getName() {
264:
265: return (name);
266:
267: }
268:
269: /**
270: * Return the session persistence pathname, if any.
271: */
272: public String getPathname() {
273:
274: return (this .pathname);
275:
276: }
277:
278: /**
279: * Set the session persistence pathname to the specified value. If no
280: * persistence support is desired, set the pathname to <code>null</code>.
281: *
282: * @param pathname New session persistence pathname
283: */
284: public void setPathname(String pathname) {
285:
286: String oldPathname = this .pathname;
287: this .pathname = pathname;
288: support.firePropertyChange("pathname", oldPathname,
289: this .pathname);
290:
291: }
292:
293: // --------------------------------------------------------- Public Methods
294:
295: /**
296: * Construct and return a new session object, based on the default
297: * settings specified by this Manager's properties. The session
298: * id will be assigned by this method, and available via the getId()
299: * method of the returned session. If a new session cannot be created
300: * for any reason, return <code>null</code>.
301: *
302: * @exception IllegalStateException if a new session cannot be
303: * instantiated for any reason
304: */
305: public Session createSession() {
306:
307: if ((maxActiveSessions >= 0)
308: && (sessions.size() >= maxActiveSessions))
309: throw new IllegalStateException(sm
310: .getString("standardManager.createSession.ise"));
311:
312: return (super .createSession());
313:
314: }
315:
316: /**
317: * Load any currently active sessions that were previously unloaded
318: * to the appropriate persistence mechanism, if any. If persistence is not
319: * supported, this method returns without doing anything.
320: *
321: * @exception ClassNotFoundException if a serialized class cannot be
322: * found during the reload
323: * @exception IOException if an input/output error occurs
324: */
325: public void load() throws ClassNotFoundException, IOException {
326:
327: if (debug >= 1)
328: log("Start: Loading persisted sessions");
329:
330: // Initialize our internal data structures
331: recycled.clear();
332: sessions.clear();
333:
334: // Open an input stream to the specified pathname, if any
335: File file = file();
336: if (file == null)
337: return;
338: if (debug >= 1)
339: log(sm.getString("standardManager.loading", pathname));
340: FileInputStream fis = null;
341: ObjectInputStream ois = null;
342: Loader loader = null;
343: ClassLoader classLoader = null;
344: try {
345: fis = new FileInputStream(file.getAbsolutePath());
346: BufferedInputStream bis = new BufferedInputStream(fis);
347: if (container != null)
348: loader = container.getLoader();
349: if (loader != null)
350: classLoader = loader.getClassLoader();
351: if (classLoader != null) {
352: if (debug >= 1)
353: log("Creating custom object input stream for class loader "
354: + classLoader);
355: ois = new CustomObjectInputStream(bis, classLoader);
356: } else {
357: if (debug >= 1)
358: log("Creating standard object input stream");
359: ois = new ObjectInputStream(bis);
360: }
361: } catch (FileNotFoundException e) {
362: if (debug >= 1)
363: log("No persisted data file found");
364: return;
365: } catch (IOException e) {
366: log(sm.getString("standardManager.loading.ioe", e), e);
367: if (ois != null) {
368: try {
369: ois.close();
370: } catch (IOException f) {
371: ;
372: }
373: ois = null;
374: }
375: throw e;
376: }
377:
378: // Load the previously unloaded active sessions
379: synchronized (sessions) {
380: try {
381: Integer count = (Integer) ois.readObject();
382: int n = count.intValue();
383: if (debug >= 1)
384: log("Loading " + n + " persisted sessions");
385: for (int i = 0; i < n; i++) {
386: StandardSession session = new StandardSession(this );
387: session.readObjectData(ois);
388: session.setManager(this );
389: sessions.put(session.getId(), session);
390: ((StandardSession) session).activate();
391: }
392: } catch (ClassNotFoundException e) {
393: log(sm.getString("standardManager.loading.cnfe", e), e);
394: if (ois != null) {
395: try {
396: ois.close();
397: } catch (IOException f) {
398: ;
399: }
400: ois = null;
401: }
402: throw e;
403: } catch (IOException e) {
404: log(sm.getString("standardManager.loading.ioe", e), e);
405: if (ois != null) {
406: try {
407: ois.close();
408: } catch (IOException f) {
409: ;
410: }
411: ois = null;
412: }
413: throw e;
414: } finally {
415: // Close the input stream
416: try {
417: if (ois != null)
418: ois.close();
419: } catch (IOException f) {
420: // ignored
421: }
422:
423: // Delete the persistent storage file
424: if (file != null && file.exists())
425: file.delete();
426: }
427: }
428:
429: if (debug >= 1)
430: log("Finish: Loading persisted sessions");
431: }
432:
433: /**
434: * Save any currently active sessions in the appropriate persistence
435: * mechanism, if any. If persistence is not supported, this method
436: * returns without doing anything.
437: *
438: * @exception IOException if an input/output error occurs
439: */
440: public void unload() throws IOException {
441:
442: if (debug >= 1)
443: log("Unloading persisted sessions");
444:
445: // Open an output stream to the specified pathname, if any
446: File file = file();
447: if (file == null)
448: return;
449: if (debug >= 1)
450: log(sm.getString("standardManager.unloading", pathname));
451: FileOutputStream fos = null;
452: ObjectOutputStream oos = null;
453: try {
454: fos = new FileOutputStream(file.getAbsolutePath());
455: oos = new ObjectOutputStream(new BufferedOutputStream(fos));
456: } catch (IOException e) {
457: log(sm.getString("standardManager.unloading.ioe", e), e);
458: if (oos != null) {
459: try {
460: oos.close();
461: } catch (IOException f) {
462: ;
463: }
464: oos = null;
465: }
466: throw e;
467: }
468:
469: // Write the number of active sessions, followed by the details
470: ArrayList list = new ArrayList();
471: synchronized (sessions) {
472: if (debug >= 1)
473: log("Unloading " + sessions.size() + " sessions");
474: try {
475: oos.writeObject(new Integer(sessions.size()));
476: Iterator elements = sessions.values().iterator();
477: while (elements.hasNext()) {
478: StandardSession session = (StandardSession) elements
479: .next();
480: list.add(session);
481: ((StandardSession) session).passivate();
482: session.writeObjectData(oos);
483: }
484: } catch (IOException e) {
485: log(sm.getString("standardManager.unloading.ioe", e), e);
486: if (oos != null) {
487: try {
488: oos.close();
489: } catch (IOException f) {
490: ;
491: }
492: oos = null;
493: }
494: throw e;
495: }
496: }
497:
498: // Flush and close the output stream
499: try {
500: oos.flush();
501: oos.close();
502: oos = null;
503: } catch (IOException e) {
504: if (oos != null) {
505: try {
506: oos.close();
507: } catch (IOException f) {
508: ;
509: }
510: oos = null;
511: }
512: throw e;
513: }
514:
515: // Expire all the sessions we just wrote
516: if (debug >= 1)
517: log("Expiring " + list.size() + " persisted sessions");
518: Iterator expires = list.iterator();
519: while (expires.hasNext()) {
520: StandardSession session = (StandardSession) expires.next();
521: try {
522: session.expire(false);
523: } catch (Throwable t) {
524: ;
525: }
526: }
527:
528: if (debug >= 1)
529: log("Unloading complete");
530:
531: }
532:
533: // ------------------------------------------------------ Lifecycle Methods
534:
535: /**
536: * Add a lifecycle event listener to this component.
537: *
538: * @param listener The listener to add
539: */
540: public void addLifecycleListener(LifecycleListener listener) {
541:
542: lifecycle.addLifecycleListener(listener);
543:
544: }
545:
546: /**
547: * Get the lifecycle listeners associated with this lifecycle. If this
548: * Lifecycle has no listeners registered, a zero-length array is returned.
549: */
550: public LifecycleListener[] findLifecycleListeners() {
551:
552: return lifecycle.findLifecycleListeners();
553:
554: }
555:
556: /**
557: * Remove a lifecycle event listener from this component.
558: *
559: * @param listener The listener to remove
560: */
561: public void removeLifecycleListener(LifecycleListener listener) {
562:
563: lifecycle.removeLifecycleListener(listener);
564:
565: }
566:
567: /**
568: * Prepare for the beginning of active use of the public methods of this
569: * component. This method should be called after <code>configure()</code>,
570: * and before any of the public methods of the component are utilized.
571: *
572: * @exception LifecycleException if this component detects a fatal error
573: * that prevents this component from being used
574: */
575: public void start() throws LifecycleException {
576:
577: if (debug >= 1)
578: log("Starting");
579:
580: // Validate and update our current component state
581: if (started)
582: throw new LifecycleException(sm
583: .getString("standardManager.alreadyStarted"));
584: lifecycle.fireLifecycleEvent(START_EVENT, null);
585: started = true;
586:
587: // Force initialization of the random number generator
588: if (debug >= 1)
589: log("Force random number initialization starting");
590: String dummy = generateSessionId();
591: if (debug >= 1)
592: log("Force random number initialization completed");
593:
594: // Load unloaded sessions, if any
595: try {
596: load();
597: } catch (Throwable t) {
598: log(sm.getString("standardManager.managerLoad"), t);
599: }
600:
601: // Start the background reaper thread
602: threadStart();
603:
604: }
605:
606: /**
607: * Gracefully terminate the active use of the public methods of this
608: * component. This method should be the last one called on a given
609: * instance of this component.
610: *
611: * @exception LifecycleException if this component detects a fatal error
612: * that needs to be reported
613: */
614: public void stop() throws LifecycleException {
615:
616: if (debug >= 1)
617: log("Stopping");
618:
619: // Validate and update our current component state
620: if (!started)
621: throw new LifecycleException(sm
622: .getString("standardManager.notStarted"));
623: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
624: started = false;
625:
626: // Stop the background reaper thread
627: threadStop();
628:
629: // Write out sessions
630: try {
631: unload();
632: } catch (IOException e) {
633: log(sm.getString("standardManager.managerUnload"), e);
634: }
635:
636: // Expire all active sessions
637: Session sessions[] = findSessions();
638: for (int i = 0; i < sessions.length; i++) {
639: StandardSession session = (StandardSession) sessions[i];
640: if (!session.isValid())
641: continue;
642: try {
643: session.expire();
644: } catch (Throwable t) {
645: ;
646: }
647: }
648:
649: // Require a new random number generator if we are restarted
650: this .random = null;
651:
652: }
653:
654: // ----------------------------------------- PropertyChangeListener Methods
655:
656: /**
657: * Process property change events from our associated Context.
658: *
659: * @param event The property change event that has occurred
660: */
661: public void propertyChange(PropertyChangeEvent event) {
662:
663: // Validate the source of this event
664: if (!(event.getSource() instanceof Context))
665: return;
666: Context context = (Context) event.getSource();
667:
668: // Process a relevant property change
669: if (event.getPropertyName().equals("sessionTimeout")) {
670: try {
671: setMaxInactiveInterval(((Integer) event.getNewValue())
672: .intValue() * 60);
673: } catch (NumberFormatException e) {
674: log(sm.getString("standardManager.sessionTimeout",
675: event.getNewValue().toString()));
676: }
677: }
678:
679: }
680:
681: // -------------------------------------------------------- Private Methods
682:
683: /**
684: * Return a File object representing the pathname to our
685: * persistence file, if any.
686: */
687: private File file() {
688:
689: if (pathname == null)
690: return (null);
691: File file = new File(pathname);
692: if (!file.isAbsolute()) {
693: if (container instanceof Context) {
694: ServletContext servletContext = ((Context) container)
695: .getServletContext();
696: File tempdir = (File) servletContext
697: .getAttribute(Globals.WORK_DIR_ATTR);
698: if (tempdir != null)
699: file = new File(tempdir, pathname);
700: }
701: }
702: // if (!file.isAbsolute())
703: // return (null);
704: return (file);
705:
706: }
707:
708: /**
709: * Invalidate all sessions that have expired.
710: */
711: private void processExpires() {
712:
713: long timeNow = System.currentTimeMillis();
714: Session sessions[] = findSessions();
715:
716: for (int i = 0; i < sessions.length; i++) {
717: StandardSession session = (StandardSession) sessions[i];
718: if (!session.isValid())
719: continue;
720: int maxInactiveInterval = session.getMaxInactiveInterval();
721: if (maxInactiveInterval < 0)
722: continue;
723: int timeIdle = // Truncate, do not round up
724: (int) ((timeNow - session.getLastAccessedTime()) / 1000L);
725: if (timeIdle >= maxInactiveInterval) {
726: try {
727: session.expire();
728: } catch (Throwable t) {
729: log(
730: sm
731: .getString("standardManager.expireException"),
732: t);
733: }
734: }
735: }
736:
737: }
738:
739: /**
740: * Sleep for the duration specified by the <code>checkInterval</code>
741: * property.
742: */
743: private void threadSleep() {
744:
745: try {
746: Thread.sleep(checkInterval * 1000L);
747: } catch (InterruptedException e) {
748: ;
749: }
750:
751: }
752:
753: /**
754: * Start the background thread that will periodically check for
755: * session timeouts.
756: */
757: private void threadStart() {
758:
759: if (thread != null)
760: return;
761:
762: threadDone = false;
763: threadName = "StandardManager[" + container.getName() + "]";
764: thread = new Thread(this , threadName);
765: thread.setDaemon(true);
766: thread.setContextClassLoader(container.getLoader()
767: .getClassLoader());
768: thread.start();
769:
770: }
771:
772: /**
773: * Stop the background thread that is periodically checking for
774: * session timeouts.
775: */
776: private void threadStop() {
777:
778: if (thread == null)
779: return;
780:
781: threadDone = true;
782: thread.interrupt();
783: try {
784: thread.join();
785: } catch (InterruptedException e) {
786: ;
787: }
788:
789: thread = null;
790:
791: }
792:
793: // ------------------------------------------------------ Background Thread
794:
795: /**
796: * The background thread that checks for session timeouts and shutdown.
797: */
798: public void run() {
799:
800: // Loop until the termination semaphore is set
801: while (!threadDone) {
802: threadSleep();
803: processExpires();
804: }
805:
806: }
807: }
|