OK, here's a start on the code for FileStore and a persistent session Manager 
class. The attached patches and new class file basically duplicate the functionality
of the current StandardManager class, but the code for loading and unloading
sessions to disk has been moved to FileStore. 

The following patches shouldn't affect default useage of Catalina. To try
them out, add the following to the server.xml <Context> section for the webapp
you want to use PersistentManager for:

          <Manager className="org.apache.catalina.session.PersistentManager" 
debug="3"/>

Store
    Added setManager() and getManager() methods to give the Store
    a reference to the Manager class which is using it. 
    To do:
         I'm thinking about adding a clear() method to this, which will remove all
         sessions from the Store.

FileStore
    Implemented getSize(), keys(), load(), remove(), clear(), save(), and various
    utility methods. Most of the code, especially for the serialization operations,
    came from StandardManager. 

    Sessions are now saved one per file. This is less efficient for loading and
    unloading all of the sessions at once, but will be better for the more interesting
    functionality which PersistentManager will have.

    To Do:
    - Move the actual serialization code to a utility class.
    - Iron out configuration with server.xml  

PersistentManager
    Currently this is a clone of StandardManager, but the load() and unload()
    functionality has been changed to use a Store object to do the work.

    To Do:
    - Add code to manage swapping out of active but idle sessions to the Store.

LocalStrings
    Added some messages for FileStore and PersistentManager

--- FileStore.java.orig Fri Aug 11 20:39:15 2000
+++ FileStore.java      Wed Dec 27 09:57:13 2000
@@ -74,18 +74,27 @@
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
 import java.io.Serializable;
 import java.util.Enumeration;
 import java.util.Hashtable;
 import java.util.Vector;
+import javax.servlet.ServletContext;
+import org.apache.catalina.Context;
+import org.apache.catalina.Globals;
 import org.apache.catalina.Lifecycle;
 import org.apache.catalina.LifecycleEvent;
 import org.apache.catalina.LifecycleException;
 import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.Loader;
+import org.apache.catalina.Logger;
+import org.apache.catalina.Manager;
 import org.apache.catalina.Session;
 import org.apache.catalina.Store;
+import org.apache.catalina.Container;
 import org.apache.catalina.util.LifecycleSupport;
 import org.apache.catalina.util.StringManager;
 
@@ -95,7 +104,6 @@
  * a file per saved Session in a configured directory.  Sessions that are
  * saved are still subject to being expired based on inactivity.
  *
- * @author Craig R. McClanahan
  * @version $Revision: 1.1 $ $Date: 2000/08/11 23:39:15 $
  */
 
@@ -103,6 +111,14 @@
     implements Lifecycle, Runnable, Store {
 
 
+    // ----------------------------------------------------- Constants
+    
+    
+    /**
+     * The extension to use for all serialized session filenames.
+     */
+    private static final String FILE_EXT = ".session";
+
 
     // ----------------------------------------------------- Instance Variables
 
@@ -116,7 +132,12 @@
     /**
      * The pathname of the directory in which Sessions are stored.
      */
-    private String directoryPath = "./STORE";
+    private String directoryPath = ".";
+
+    /**
+     * A File representing the directory in which Sessions are stored.
+     */
+    private File directoryFile = null;
 
 
     /**
@@ -163,6 +184,18 @@
 
 
     /**
+     * The Manager with which this FileStore is associated.
+     */
+    protected Manager manager;
+
+
+    /**
+     * The debugging detail level for this component.
+     */
+    protected int debug = 0;
+
+
+    /**
      * Name to register for the background thread.
      */
     private String threadName = "FileStore";
@@ -216,6 +249,7 @@
 
        String oldDirectoryPath = this.directoryPath;
        this.directoryPath = path;
+       this.directoryFile = null;
        support.firePropertyChange("directoryPath", oldDirectoryPath,
                                   this.directoryPath);
 
@@ -241,7 +275,16 @@
      */
     public int getSize() throws IOException {
 
-       return (0);     // FIXME: getSize()
+       String[] files = getDirectoryFile().list();
+       
+       // Figure out which files are sessions
+       int keycount = 0;
+       for (int i = 0; i < files.length; i++) {
+               if (files[i].endsWith(FILE_EXT))
+                       keycount++;
+       }
+       
+       return (keycount);
 
     }
 
@@ -270,7 +313,30 @@
      */
     public String[] keys() throws IOException {
 
-       return (new String[0]); // FIXME: keys()
+       String[] files = getDirectoryFile().list();
+       
+       // Figure out which files contain sessions
+       int keycount = 0;
+       for (int i = 0; i < files.length; i++) {
+               if (files[i].endsWith(FILE_EXT))
+                       keycount++;
+               else
+                       files[i] = null;
+       }
+
+       // Get keys from relevant filenames.
+       String[] keys = new String[keycount];
+       if (keycount > 0) {
+               keycount = 0;
+               for (int i = 0; i < files.length; i++) {
+                   if (files[i] != null) {
+                       keys[keycount] = files[i].substring (0, 
+files[i].lastIndexOf('.'));
+                       keycount++;
+                   }
+               }
+       }
+
+       return (keys);
 
     }
 
@@ -288,11 +354,66 @@
     public Session load(String id)
         throws ClassNotFoundException, IOException {
 
-       return (null);  // FIXME: load()
+       // FIXME: Should this return null if the Session is past
+       // its date? Configurable option?
 
-    }
+       // Open an input stream to the specified pathname, if any
+       File file = file(id);
+       if (file == null)
+           return (null);
+       if (debug >= 1)
+           log(sm.getString("fileStore.loading", id, file.getAbsolutePath()));
+
+       FileInputStream fis = null;
+       ObjectInputStream ois = null;
+       Loader loader = null;
+       ClassLoader classLoader = null;
+       try {
+           fis = new FileInputStream(file.getAbsolutePath());
+           BufferedInputStream bis = new BufferedInputStream(fis);
+           Container container = manager.getContainer();
+           if (container != null)
+               loader = container.getLoader();
+           if (loader != null)
+               classLoader = loader.getClassLoader();
+           if (classLoader != null)
+               ois = new CustomObjectInputStream(bis, classLoader);
+           else
+               ois = new ObjectInputStream(bis);
+       } catch (FileNotFoundException e) {
+            if (debug >= 1)
+                log("No persisted data file found");
+           return (null);
+       } catch (IOException e) {
+           if (ois != null) {
+               try {
+                   ois.close();
+               } catch (IOException f) {
+                   ;
+               }
+               ois = null;
+           }
+           throw e;
+       }
 
+       try {
+           StandardSession session = new StandardSession(manager);
+            session.readObjectData(ois);
+           session.setManager(manager);
+           return (session);
+       } finally {
+           // Close the input stream
+           if (ois != null) {
+               try {
+                   ois.close();
+               } catch (IOException f) {
+                   ;
+               }
+           }
+       }
+    }
 
+    
     /**
      * Remove the Session with the specified session identifier from
      * this Store, if present.  If no such Session is present, this method
@@ -304,12 +425,36 @@
      */
     public void remove(String id) throws IOException {
 
-       ;       // FIXME: remove()
-
+       // Open an input stream to the specified pathname, if any
+       File file = file(id);
+       if (file == null)
+           return;
+       if (debug >= 1)
+           log(sm.getString("fileStore.removing", id, file.getAbsolutePath()));
+       file.delete();
     }
 
 
     /**
+     * Remove all of the Sessions in this Store.
+     *
+     * @exception ClassNotFoundException if a deserialization error occurs
+     * @exception IOException if an input/output error occurs
+     */
+    public void clear()
+        throws ClassNotFoundException, IOException {
+
+       // FIXME: Should this be in the interface?
+
+       String[] keys = keys();
+       for (int i = 0; i < keys.length; i++) {
+           remove(keys[i]);
+           // FIXME: Check whether the session is null first?
+       }
+
+    }
+    
+    /**
      * Remove a property change listener from this component.
      *
      * @param listener The listener to remove
@@ -331,7 +476,38 @@
      */
     public void save(Session session) throws IOException {
 
-       ;       // FIXME: save()
+       // FIXME: Should this do nothing if the session is past
+       // its expiry?
+
+       // Open an output stream to the specified pathname, if any
+       File file = file(session.getId());
+       if (file == null)
+           return;
+       if (debug >= 1)
+           log(sm.getString("fileStore.saving", session.getId(), 
+file.getAbsolutePath()));
+       FileOutputStream fos = null;
+       ObjectOutputStream oos = null;
+       try {
+           fos = new FileOutputStream(file.getAbsolutePath());
+           oos = new ObjectOutputStream(new BufferedOutputStream(fos));
+       } catch (IOException e) {
+           if (oos != null) {
+               try {
+                   oos.close();
+               } catch (IOException f) {
+                   ;
+               }
+           }
+           throw e;
+       }
+
+       try {
+           ((StandardSession)session).writeObjectData(oos);
+           oos.flush();
+       } finally {
+           if (oos != null)
+               oos.close();
+       }
 
     }
 
@@ -412,31 +588,167 @@
     }
 
 
+    /**
+     * Return the Manager with which the FileStore is associated.
+     */
+    public Manager getManager() {
+    
+       return (this.manager);
+       
+    }
+
+    /**
+     * Set the Manager with which this FileStore is associated.
+     *
+     * @param manager The newly associated Manager
+     */
+    public void setManager(Manager manager) {
+
+       Manager oldManager = this.manager;
+       this.manager = manager;
+       support.firePropertyChange("manager", oldManager, this.manager);
+
+    }
+
+
+    /**
+     * Return the debugging detail level for this component.
+     */
+    public int getDebug() {
+
+       return (this.debug);
+
+    }
+
+
+    /**
+     * Set the debugging detail level for this component.
+     *
+     * @param debug The new debugging detail level
+     */
+    public void setDebug(int debug) {
+
+       this.debug = debug;
+
+    }
+
+    /**
+     * Log a message on the Logger associated with our Container (if any).
+     *
+     * @param message Message to be logged
+     */
+    void log(String message) {
+
+       Logger logger = null;
+       Container container = manager.getContainer();
+       if (container != null)
+           logger = container.getLogger();
+       if (logger != null)
+           logger.log("Manager[" + container.getName() + "]: "
+                      + message);
+       else {
+           String containerName = null;
+           if (container != null)
+               containerName = container.getName();
+           System.out.println("Manager[" + containerName
+                              + "]: " + message);
+       }
+
+    }
+
+
     // -------------------------------------------------------- Private Methods
 
+    /**
+     * Return a File object representing the pathname to our
+     * session persistence file, if any.
+     *
+     * @param id The ID of the Session to be retrieved. This is
+     *    used in the file naming.
+     */
+    private File file(String id) {
+
+       if (directoryPath == null)
+           return (null);
+
+       String pathname = directoryPath + "/" + id + FILE_EXT;
+       File file = new File(pathname);
+       if (!file.isAbsolute()) {
+           File tempdir = getDirectoryFile();
+           if (tempdir != null)
+               file = new File(tempdir, pathname);
+       }
+       return (file);
+
+// FIXME: It would be nice to keep this check, but
+// it doesn't work under Windows on paths that start
+// with a drive letter.
+//     if (!file.isAbsolute())
+//         return (null);
+//     return (file);
+
+    }
 
     /**
+     * Return a File object for the directoryPath property.
+     */
+    private File getDirectoryFile() {
+    
+       if (directoryFile == null) {
+           Container container = manager.getContainer();
+           if (container instanceof Context) {
+               ServletContext servletContext =
+                   ((Context) container).getServletContext();
+               directoryFile = (File)
+                   servletContext.getAttribute(Globals.WORK_DIR_ATTR);
+           }
+       }
+       
+       return directoryFile;
+
+    }
+               
+    /**
      * Invalidate all sessions that have expired.
      */
     private void processExpires() {
 
        long timeNow = System.currentTimeMillis();
-       /*
-       Session sessions[] = findSessions();
-
-       for (int i = 0; i < sessions.length; i++) {
-           StandardSession session = (StandardSession) sessions[i];
-           if (!session.isValid())
-               continue;
-           int maxInactiveInterval = session.getMaxInactiveInterval();
-           if (maxInactiveInterval < 0)
-               continue;
-           int timeIdle = // Truncate, do not round up
+       String[] keys = null;
+       
+       try {
+           keys = keys();
+       } catch (IOException e) {
+               // FIXME: Do something nicer here
+               log (e.toString());
+               e.printStackTrace();
+               return;
+       }
+       
+       for (int i = 0; i < keys.length; i++) {
+           try {
+               StandardSession session = (StandardSession) load(keys[i]);
+               if (!session.isValid())
+                   continue;
+               int maxInactiveInterval = session.getMaxInactiveInterval();
+               if (maxInactiveInterval < 0)
+                   continue;
+               int timeIdle = // Truncate, do not round up
                (int) ((timeNow - session.getLastAccessedTime()) / 1000L);
-           if (timeIdle >= maxInactiveInterval)
-               session.expire();
+               if (timeIdle >= maxInactiveInterval) {
+                   session.expire();
+                   remove(session.getId());
+               }
+           } catch (IOException e) {
+               // FIXME: Do something nicer here
+               log (e.toString());
+               e.printStackTrace();
+           } catch (ClassNotFoundException e) {
+               // FIXME: Do something nicer here
+               log (e.toString());
+               e.printStackTrace();
+           }
        }
-       */
 
     }
 
@@ -511,5 +823,59 @@
 
     }
 
+    // -------------------------------------------------------- Private Classes
+
+
+    /**
+     * Custom subclass of <code>ObjectInputStream</code> that loads from the
+     * class loader for this web application.  This allows classes defined only
+     * with the web application to be found correctly.
+     */
+    private static final class CustomObjectInputStream
+       extends ObjectInputStream {
+
+
+       /**
+        * The class loader we will use to resolve classes.
+        */
+       private ClassLoader classLoader = null;
+
+
+       /**
+        * Construct a new instance of CustomObjectInputStream
+        *
+        * @param stream The input stream we will read from
+        * @param classLoader The class loader used to instantiate objects
+        *
+        * @exception IOException if an input/output error occurs
+        */
+       public CustomObjectInputStream(InputStream stream,
+                                      ClassLoader classLoader)
+           throws IOException {
+
+           super(stream);
+           this.classLoader = classLoader;
+
+       }
+
+
+       /**
+        * Load the local class equivalent of the specified stream class
+        * description, by using the class loader assigned to this Context.
+        *
+        * @param classDesc Class description from the input stream
+        *
+        * @exception ClassNotFoundException if this class cannot be found
+        * @exception IOException if an input/output error occurs
+        */
+       protected Class resolveClass(ObjectStreamClass classDesc)
+           throws ClassNotFoundException, IOException {
+
+           return (classLoader.loadClass(classDesc.getName()));
+
+       }
+
+
+    }
 
 }
--- LocalStrings.properties.orig        Fri Aug 18 19:21:58 2000
+++ LocalStrings.properties     Wed Dec 27 09:23:59 2000
@@ -2,6 +2,9 @@
 applicationSession.value.iae=null value
 fileStore.alreadyStarted=File Store has already been started
 fileStore.notStarted=File Store has not yet been started
+fileStore.saving=Saving Session {0} to file {1}
+fileStore.loading=Loading Session {0} from file {1}
+fileStore.removing=Removing Session {0} at file {1}
 managerBase.complete=Seeding of random number generator has been completed
 managerBase.getting=Getting message digest component for algorithm {0}
 managerBase.gotten=Completed getting message digest component
@@ -27,3 +30,7 @@
 standardSession.sessionEvent=Session event listener threw exception
 standardSession.setAttribute.ise=setAttribute: Non-serializable attribute
 standardSession.setAttribute.ise=setAttribute: Session already invalidated
+persistentManager.loading=Loading {0} persisted sessions
+persistentManager.unloading=Saving {0} persisted sessions
+persistentManager.expiring=Expiring {0} sessions before saving them
+persistentManager.deserializeError=Error deserializing Session {0}
/*
 * $Header$
 * $Revision$
 * $Date$
 *
 * ====================================================================
 *
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999 The Apache Software Foundation.  All rights 
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:  
 *       "This product includes software developed by the 
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written 
 *    permission, please contact [EMAIL PROTECTED]
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 * [Additional notices, if required by prior licensing conditions]
 *
 */ 


package org.apache.catalina.session;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.util.ArrayList;
import java.util.Iterator;
import javax.servlet.ServletContext;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Loader;
import org.apache.catalina.Manager;
import org.apache.catalina.Session;
import org.apache.catalina.Store;
import org.apache.catalina.util.LifecycleSupport;


/**
 * Implementation of the <b>Manager</b> interface that makes use of
 * a Store to swap active Sessions to disk. It can be configured to
 * achieve several different goals:
 *
 * <li>Persist sessions across restarts of the Container</li>
 * <li>Fault tolerance, keep sessions backed up on disk to allow
 *     recovery in the event of unplanned restarts.</li>
 * <li>Limit the number of active sessions kept in memory by
 *     swapping less active sessions out to disk.</li>
 *
 * @version $Revision$
 */

public final class PersistentManager
    extends ManagerBase
    implements Lifecycle, PropertyChangeListener, Runnable {


    // ----------------------------------------------------- Instance Variables


    /**
     * The interval (in seconds) between checks for expired sessions.
     */
    private int checkInterval = 60;


    /**
     * The descriptive information about this implementation.
     */
    private static final String info = "DistributedManager/1.0";


    /**
     * The lifecycle event support for this component.
     */
    protected LifecycleSupport lifecycle = new LifecycleSupport(this);


    /**
     * The maximum number of active Sessions allowed, or -1 for no limit.
     */
    private int maxActiveSessions = -1;


    /**
     * Has this component been started yet?
     */
    private boolean started = false;


    /**
     * The background thread.
     */
    private Thread thread = null;


    /**
     * The background thread completion semaphore.
     */
    private boolean threadDone = false;


    /**
     * Name to register for the background thread.
     */
    private String threadName = "DistributedManager";


    /**
     * Store object which will manage the Session store.
     */
    private Store store = null;

    // ------------------------------------------------------------- Properties


    /**
     * Return the check interval (in seconds) for this Manager.
     */
    public int getCheckInterval() {

        return (this.checkInterval);

    }


    /**
     * Set the check interval (in seconds) for this Manager.
     *
     * @param checkInterval The new check interval
     */
    public void setCheckInterval(int checkInterval) {

        int oldCheckInterval = this.checkInterval;
        this.checkInterval = checkInterval;
        support.firePropertyChange("checkInterval",
                                   new Integer(oldCheckInterval),
                                   new Integer(this.checkInterval));

    }


    /**
     * Set the Container with which this Manager has been associated.  If
     * it is a Context (the usual case), listen for changes to the session
     * timeout property.
     *
     * @param container The associated Container
     */
    public void setContainer(Container container) {

        // De-register from the old Container (if any)
        if ((this.container != null) && (this.container instanceof Context))
            ((Context) this.container).removePropertyChangeListener(this);

        // Default processing provided by our superclass
        super.setContainer(container);

        // Register with the new Container (if any)
        if ((this.container != null) && (this.container instanceof Context)) {
            setMaxInactiveInterval
                ( ((Context) this.container).getSessionTimeout()*60 );
            ((Context) this.container).addPropertyChangeListener(this);
        }

    }


    /**
     * Set the Store object which will manage persistent Session
     * storage for this Manager.
     *
     * @param store the associated Store
     */
    public void setStore(Store store) {
    
        this.store = store;
        
    }
    
    /**
     * Return the Store object which manages persistent Session
     * storage for this Manager.
     */
    public Store getStore() {
        
        return (this.store);
        
    }

    /**
     * Return descriptive information about this Manager implementation and
     * the corresponding version number, in the format
     * <code>&lt;description&gt;/&lt;version&gt;</code>.
     */
    public String getInfo() {

        return (this.info);

    }


    /**
     * Return the maximum number of active Sessions allowed, or -1 for
     * no limit.
     */
    public int getMaxActiveSessions() {

        return (this.maxActiveSessions);

    }


    /**
     * Set the maximum number of actives Sessions allowed, or -1 for
     * no limit.
     *
     * @param max The new maximum number of sessions
     */
    public void setMaxActiveSessions(int max) {

        int oldMaxActiveSessions = this.maxActiveSessions;
        this.maxActiveSessions = max;
        support.firePropertyChange("maxActiveSessions",
                                   new Integer(oldMaxActiveSessions),
                                   new Integer(this.maxActiveSessions));

    }


    // --------------------------------------------------------- Public Methods


    /**
     * Return the active Session, associated with this Manager, with the
     * specified session id (if any); otherwise return <code>null</code>.
     *
     * @param id The session id for the session to be returned
     *
     * @exception IllegalStateException if a new session cannot be
     *  instantiated for any reason
     * @exception IOException if an input/output error occurs while
     *  processing this request
     */
    public Session findSession(String id) throws IOException {

        Session session = super.findSession(id);

        // FIXME: Only do this if our settings indicate we
        // might have swapped idle sessions out.
        if (session == null) {
            try {
                session = store.load(id);
            } catch (ClassNotFoundException e) {
                log (sm.getString("persistentManager.deserializeError", id));
                throw new IllegalStateException ("Can't deserialize session");
            }
        }

        if (session == null) 
                return (null);

        session.setManager(this);
        sessions.put(session.getId(), session);
        return (session);
    }
        
    /**
     * Load any currently active sessions that were previously unloaded
     * to the appropriate persistence mechanism, if any.  If persistence is not
     * supported, this method returns without doing anything.
     *
     * @exception ClassNotFoundException if a serialized class cannot be
     *  found during the reload
     * @exception IOException if an input/output error occurs
     */
    public void load() throws ClassNotFoundException, IOException {

        if (store == null)
                return;

        // Initialize our internal data structures
        recycled.clear();
        sessions.clear();

        String[] ids = store.keys();

        // Load the previously unloaded active sessions
        synchronized (sessions) {
            int n = ids.length;
            if (debug >= 1)
                log(sm.getString("persistentManager.loading", String.valueOf(n)));
            for (int i = 0; i < n; i++) {
                StandardSession session = (StandardSession) store.load (ids[i]);
                // FIXME: Check whether the session is valid, not expired, discard if 
not.
                session.setManager(this);
                sessions.put(session.getId(), session);
            }
        }
    }


    /**
     * Save all currently active sessions in the appropriate persistence
     * mechanism, if any.  If persistence is not supported, this method
     * returns without doing anything.
     *
     * @exception IOException if an input/output error occurs
     */
    public void unload() throws IOException {

        ArrayList list = new ArrayList();
        synchronized (sessions) {
            if (debug >= 1)
                log(sm.getString("persistentManager.unloading", 
String.valueOf(sessions.size())));
            Iterator elements = sessions.values().iterator();
            while (elements.hasNext()) {
                StandardSession session =
                    (StandardSession) elements.next();
                list.add(session);
                store.save(session);
            }
        }

        // Expire all the sessions we just wrote
        if (debug >= 1)
            log(sm.getString("persistentManager.expiring", 
String.valueOf(list.size())));
        Iterator expires = list.iterator();
        while (expires.hasNext()) {
            StandardSession session = (StandardSession) expires.next();
            try {
                session.expire();
            } catch (Throwable t) {
                ;
            }
        }

        if (debug >= 1)
            log("Unloading complete");

    }


    // ------------------------------------------------------ Lifecycle Methods


    /**
     * Add a lifecycle event listener to this component.
     *
     * @param listener The listener to add
     */
    public void addLifecycleListener(LifecycleListener listener) {

        lifecycle.addLifecycleListener(listener);

    }


    /**
     * Remove a lifecycle event listener from this component.
     *
     * @param listener The listener to remove
     */
    public void removeLifecycleListener(LifecycleListener listener) {

        lifecycle.removeLifecycleListener(listener);

    }


    /**
     * Prepare for the beginning of active use of the public methods of this
     * component.  This method should be called after <code>configure()</code>,
     * and before any of the public methods of the component are utilized.
     *
     * @exception IllegalStateException if this component has already been
     *  started
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    public void start() throws LifecycleException {

        if (debug >= 1)
            log("Starting PersistentManager");

        // Validate and update our current component state
        if (started)
            throw new LifecycleException
                (sm.getString("standardManager.alreadyStarted"));
        lifecycle.fireLifecycleEvent(START_EVENT, null);
        started = true;

        // Force initialization of the random number generator
        if (debug >= 1)
            log("Force random number initialization starting");
        String dummy = generateSessionId();
        if (debug >= 1)
            log("Force random number initialization completed");

        // Create the FileStore object.
        // FIXME: Do this properly (configurable)
        store = new FileStore();
        store.setManager (this);
        if (store instanceof Lifecycle)
            ((Lifecycle)store).start();

        // Start the background reaper thread
        threadStart();

    }


    /**
     * Gracefully terminate the active use of the public methods of this
     * component.  This method should be the last one called on a given
     * instance of this component.
     *
     * @exception IllegalStateException if this component has not been started
     * @exception LifecycleException if this component detects a fatal error
     *  that needs to be reported
     */
    public void stop() throws LifecycleException {

        if (debug >= 1)
            log("Stopping PersistentManager");

        // Validate and update our current component state
        if (!started)
            throw new LifecycleException
                (sm.getString("standardManager.notStarted"));
        lifecycle.fireLifecycleEvent(STOP_EVENT, null);
        started = false;

        // Stop the background reaper thread
        threadStop();

        // Expire all active sessions
        Session sessions[] = findSessions();
        for (int i = 0; i < sessions.length; i++) {
            StandardSession session = (StandardSession) sessions[i];
            if (!session.isValid())
                continue;
            session.expire();
        }

        // Require a new random number generator if we are restarted
        this.random = null;
        
        if (store instanceof Lifecycle)
            ((Lifecycle)store).stop();

    }


    // ----------------------------------------- PropertyChangeListener Methods


    /**
     * Process property change events from our associated Context.
     *
     * @param event The property change event that has occurred
     */
    public void propertyChange(PropertyChangeEvent event) {

        // Validate the source of this event
        if (!(event.getSource() instanceof Context))
            return;
        Context context = (Context) event.getSource();

        // Process a relevant property change
        if (event.getPropertyName().equals("sessionTimeout")) {
            try {
                setMaxInactiveInterval
                    ( ((Integer) event.getNewValue()).intValue()*60 );
            } catch (NumberFormatException e) {
                log(sm.getString("standardManager.sessionTimeout",
                                 event.getNewValue().toString()));
            }
        }

    }


    // -------------------------------------------------------- Private Methods


    /**
     * Invalidate all sessions that have expired.
     */
    private void processExpires() {

        long timeNow = System.currentTimeMillis();
        Session sessions[] = findSessions();

        for (int i = 0; i < sessions.length; i++) {
            StandardSession session = (StandardSession) sessions[i];
            if (!session.isValid())
                continue;
            int maxInactiveInterval = session.getMaxInactiveInterval();
            if (maxInactiveInterval < 0)
                continue;
            int timeIdle = // Truncate, do not round up
                (int) ((timeNow - session.getLastAccessedTime()) / 1000L);
            if (timeIdle >= maxInactiveInterval) {
                session.expire();
            }
        }
    }


    /**
     * Sleep for the duration specified by the <code>checkInterval</code>
     * property.
     */
    private void threadSleep() {

        try {
            Thread.sleep(checkInterval * 1000L);
        } catch (InterruptedException e) {
            ;
        }

    }


    /**
     * Start the background thread that will periodically check for
     * session timeouts.
     */
    private void threadStart() {

        if (thread != null)
            return;

        threadDone = false;
        threadName = "PersistentManager[" + container.getName() + "]";
        thread = new Thread(this, threadName);
        thread.setDaemon(true);
        thread.start();

    }


    /**
     * Stop the background thread that is periodically checking for
     * session timeouts.
     */
    private void threadStop() {

        if (thread == null)
            return;

        threadDone = true;
        thread.interrupt();
        try {
            thread.join();
        } catch (InterruptedException e) {
            ;
        }

        thread = null;

    }


    // ------------------------------------------------------ Background Thread


    /**
     * The background thread that checks for session timeouts and shutdown.
     */
    public void run() {

        // Loop until the termination semaphore is set
        while (!threadDone) {
            threadSleep();
            processExpires();
        }

    }


}
--- Store.java.orig     Fri Aug 11 02:24:12 2000
+++ Store.java  Wed Dec 27 09:50:50 2000
@@ -68,7 +68,6 @@
 import java.beans.PropertyChangeListener;
 import java.io.IOException;
 
-
 /**
  * A <b>Store</b> is the abstraction of a Catalina component that provides
  * persistent storage and loading of Sessions and their associated user data.
@@ -92,6 +91,12 @@
      * <code>&lt;description&gt;/&lt;version&gt;</code>.
      */
     public String getInfo();
+    
+
+    /**
+     * Return the Manager instance associated with this Store.
+     */
+    public Manager getManager();
 
 
     /**
@@ -167,5 +172,11 @@
      */
     public void save(Session session) throws IOException;
 
+    /**
+     * Set the Manager associated with this Store. 
+     *
+     * @param manager The Manager which will use this Store.
+     */
+    public void setManager(Manager manager);
 
 }
---
Kief Morris
Director of Technology
bitBull Ltd. http://www.bitBull.com
phone +44 020 7598 9938 
fax  +44 020 7460 4081

Reply via email to