These patches are my proposal for session persistence in Catalina 4.1. Please
look these over and give me your comments, and commit them if they look OK.

The main change is the addition of a PersistentManager class, an optional replacement
for StandardManager, providing the following configurable functionality:

- Saves sessions to a Store on shutdown, loads on restart. This is currently 
implemented 
  in StandardManager, but PersistentManager makes use of the Store interface.
- Swaps sessions out to a Store if the number in memory exceeds maxActiveSessions,
  to better manage resources. A minIdleSwap property prevents sessions from being
  swapped out if they haven't been idle for very long, to avoid thrashing.
- Swaps sessions out to a Store if they are idle for too long. With this feature, an 
app
  can have very long session expiration times without over-burdening memory.
- Backs sessions up to Store (but keeps them in memory) if they are idle for too long.
  This provides basic fault tolerance, since the sessions will be read in from the 
Store
  when the server starts after a crash. NOT IMPLEMENTED YET! There is an issue
  regarding the Servlet 2.3 specification which I will discuss below.

To test this out, see the server.xml file - there is a section to uncomment and tweak
the configuration.

ISSUES:
----------------------------------------

- Backing up Sessions: I believe that HttpSessionActivationListeners should be notified
  when sessions are being backed up and restored. However, application developers
  reading the Servlet 2.3 specification [PFD] might not expect a session to remain
  in active use after the sessionWillPassivate() method is called. I would like to get 
  some feedback on this.

- I still have not figured out how to get Catalina to instantiate the Store class for 
me,
  this patch hard-codes the use of FileStore in PersistentManager. I've tried mucking
  around in startup/Catalina.java, to no avail. Can anyone help me out with this?

- There is a lot of duplication of code between PersistentManager and StandardManager.
  Maybe this should be moved to ManagerBase?

- The use of Session/StandardSession is very messy in ManagerBase, StandardManager, 
  and now PersistentManager. Although some code uses the Session interface and executes
  StandardSession-specific code only after testing, much of this code will break if an
  alternative implementation is used (essential properties aren't set, etc.), and some
  code has StandardSession hard-coded without tests. 
      It looks as if someone had good intentions to make it possible to replace 
StandardSession 
  with a different implementation, but it hasn't been followed through, so it's just a 
mess.
  This means I can't create a PersistentSession class and keep persistence-related code
  out of StandardSession, which is too bad. 
      Fixing this will require a fair amount of refactoring.

- While developing apps you would sometimes like to wipe out all of the sessions,
  which is a bit awkward to do with persistence enabled because the server saves
  and restores them on restarts. Currently you would have to change the server.xml 
  configuration for PersistentManager to disable all persistence and backups, and then 
  it will clear out the Store when you restart. You can manually delete them from the 
  directory used by FileStore, but this won't work with other Store implementations.
      Ideally there should be a command line argument which causes the Store to be 
  cleared.

PATCH COMMENTS
----------------------------------------

Store.java
----------------------------------------
- Added getManager(), setManager(), and clear() methods.

FileStore.java
----------------------------------------
- Implemented Store functionality, sessions are serialized one per file.

PersistentManager
----------------------------------------
- Implemented persistence across server restarts
- Implemented swapping out of sessions to manage resources, based on
  the number of sessions in memory and idle times.
- Stubs for backing up of sessions based on idle times, not yet implemented.

server.xml
----------------------------------------
- Added a commented out section to enable and configure PersistentManager

LocalStrings.properties
----------------------------------------
- Added strings for logging by FileStore and PersistentManager.


Regards,
Kief
--- FileStore.java.orig Fri Aug 11 20:39:15 2000
+++ FileStore.java      Fri Jan 12 21:55:02 2001
@@ -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;
 
@@ -103,6 +112,14 @@
     implements Lifecycle, Runnable, Store {
 
 
+    // ----------------------------------------------------- Constants
+    
+    
+    /**
+     * The extension to use for serialized session filenames.
+     */
+    private static final String FILE_EXT = ".session";
+
 
     // ----------------------------------------------------- Instance Variables
 
@@ -115,8 +132,14 @@
 
     /**
      * The pathname of the directory in which Sessions are stored.
+     * Relative to the temp directory for the web application.
+     */
+    private String directory = ".";
+
+    /**
+     * A File representing the directory in which Sessions are stored.
      */
-    private String directoryPath = "./STORE";
+    private File directoryFile = null;
 
 
     /**
@@ -163,6 +186,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";
@@ -200,9 +235,9 @@
     /**
      * Return the directory path for this Store.
      */
-    public String getDirectoryPath() {
+    public String getDirectory() {
 
-       return (directoryPath);
+       return (directory);
 
     }
 
@@ -212,12 +247,13 @@
      *
      * @param path The new directory path
      */
-    public void setDirectoryPath(String path) {
+    public void setDirectory(String path) {
 
-       String oldDirectoryPath = this.directoryPath;
-       this.directoryPath = path;
-       support.firePropertyChange("directoryPath", oldDirectoryPath,
-                                  this.directoryPath);
+       String oldDirectory = this.directory;
+       this.directory = path;
+       this.directoryFile = null;
+       support.firePropertyChange("directory", oldDirectory,
+                                  this.directory);
 
     }
 
@@ -241,7 +277,62 @@
      */
     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);
+
+    }
+
+
+    /**
+     * 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;
 
     }
 
@@ -270,7 +361,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 +402,63 @@
     public Session load(String id)
         throws ClassNotFoundException, IOException {
 
-       return (null);  // FIXME: load()
+       // 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 = (StandardSession) manager.createSession();
+            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 +470,32 @@
      */
     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 IOException if an input/output error occurs
+     */
+    public void clear()
+        throws IOException {
+
+       String[] keys = keys();
+       for (int i = 0; i < keys.length; i++) {
+           remove(keys[i]);
+       }
+
+    }
+    
+    /**
      * Remove a property change listener from this component.
      *
      * @param listener The listener to remove
@@ -331,7 +517,33 @@
      */
     public void save(Session session) throws IOException {
 
-       ;       // FIXME: save()
+       // 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);
+       } finally {
+           oos.close();
+       }
 
     }
 
@@ -412,31 +624,125 @@
     }
 
 
+    /**
+     * 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 (directory == null)
+           return (null);
+
+       String pathname = directory + "/" + 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 directory 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);
+           } else {
+               throw new IllegalArgumentException("directory not set, I can't work 
+with this Container");
+           }
+       }
+
+       return directoryFile;
+
+    }
+               
+    /**
      * Invalidate all sessions that have expired.
      */
     private void processExpires() {
+    
+       if(!started)
+           return;
 
        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) {
+           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) {
+               log (e.toString());
+               e.printStackTrace();
+           } catch (ClassNotFoundException e) {
+               log (e.toString());
+               e.printStackTrace();
+           }
        }
-       */
 
     }
 
@@ -511,5 +817,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     Fri Jan 12 21:36:30 2001
@@ -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,15 @@
 standardSession.sessionEvent=Session event listener threw exception
 standardSession.setAttribute.ise=setAttribute: Non-serializable attribute
 standardSession.setAttribute.ise=setAttribute: Session already invalidated
+standardSession.sessionCreated=Created Session id = {0}
+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}: {1}
+persistentManager.serializeError=Error deserializing Session {0}: {1}
+persistentManager.swapMaxIdle=Swapping session {0} to Store, idle for {1} seconds
+persistentManager.tooManyActive=Too many active sessions, {0}, looking for idle 
+sessions to swap out
+persistentManager.swapTooManyActive=Swapping out session {0}, idle for {1} seconds 
+too many sessions active
+persistentManager.processSwaps=Checking for sessions to swap out, {0} active sessions 
+in memory
+persistentManager.activeSession=Session {0} has been idle for {1} seconds
+persistentManager.swapIn=Swapping session {0} in from Store
--- Store.java.orig     Fri Aug 11 02:24:12 2000
+++ Store.java  Fri Jan 12 10:10:42 2001
@@ -92,6 +92,20 @@
      * <code>&lt;description&gt;/&lt;version&gt;</code>.
      */
     public String getInfo();
+    
+
+    /**
+     * Return the Manager instance associated with this Store.
+     */
+    public Manager getManager();
+
+
+    /**
+     * Set the Manager associated with this Store. 
+     *
+     * @param manager The Manager which will use this Store.
+     */
+    public void setManager(Manager manager);
 
 
     /**
@@ -147,6 +161,12 @@
      * @exception IOException if an input/output error occurs
      */
     public void remove(String id) throws IOException;
+    
+    
+    /**
+     * Remove all Sessions from this Store.
+     */
+    public void clear() throws IOException;
 
 
     /**
--- server.xml.orig     Sat Dec 16 20:03:29 2000
+++ server.xml  Fri Jan 12 22:01:04 2001
@@ -179,6 +179,53 @@
           <Ejb   name="ejb/EmplRecord" type="Entity"
                  home="com.wombat.empl.EmployeeRecordHome"
                remote="com.wombat.empl.EmployeeRecord"/>
+        <!-- 
+                       PersistentManager
+                       Uncomment the section below to test Persistent Sessions.
+                         
+                       saveOnRestart: If true, all active sessions will be saved
+                               to the Store when Catalina is shutdown, regardless of
+                               other settings. All Sessions found in the Store will 
+be 
+                               loaded on startup. Sessions past their expiration are
+                               ignored in both cases.
+                       maxActiveSessions: If 0 or greater, having too many active 
+                               sessions will result in some being swapped out. 
+minIdleSwap
+                               limits this. -1 means unlimited sessions are allowed.
+                               0 means sessions will almost always be swapped out 
+after
+                               use - this will be noticeably slow for your users.
+                       minIdleSwap: Sessions must be idle for at least this long
+                               (in seconds) before they will be swapped out due to 
+                               maxActiveSessions. This avoids thrashing when the site 
+is 
+                               highly active. -1 or 0 means there is no minimum - 
+sessions
+                               can be swapped out at any time.
+                       maxIdleSwap: Sessions will be swapped out if idle for this
+                               long (in seconds). If minIdleSwap is higher, then it 
+will
+                               override this. This isn't exact: it is checked 
+periodically.
+                               -1 means sessions won't be swapped out for this reason,
+                               although they may be swapped out for maxActiveSessions.
+                               If set to >= 0, guarantees that all sessions found in 
+the
+                               Store will be loaded on startup.
+                       maxIdleBackup: Sessions will be backed up (saved to the Store,
+                               but left in active memory) if idle for this long (in 
+seconds), 
+                               and all sessions found in the Store will be loaded on 
+startup.
+                               If set to -1 sessions will not be backed up, 0 means 
+they
+                               should be backed up shortly after being used.
+
+                       To clear sessions from the Store, set maxActiveSessions, 
+maxIdleSwap,
+                       and minIdleBackup all to -1, saveOnRestart to false, then 
+restart 
+                       Catalina.
+               -->
+               <!--
+          <Manager className="org.apache.catalina.session.PersistentManager"
+                   debug="0"
+                                  saveOnRestart="true"
+                                  maxActiveSessions="-1"
+                                  minIdleSwap="-1"
+                                  maxIdleSwap="-1"
+                                  maxIdleBackup="-1">
+              <Store className="org.apache.catalina.session.FileStore"/>
+          </Manager>
+               -->
           <Environment name="maxExemptions" type="java.lang.Integer"
                       value="15"/>
           <Parameter name="context.param.name" value="context.param.value"
/*
 * $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$
 * @author Kief Morris ([EMAIL PROTECTED])
 */

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 = "PersistentManager/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 = "PersistentManager";


    /**
     * Whether to save and reload sessions when the Manager <code>unload</code>
     * and <code>load</code> methods are called.
     */
    private boolean saveOnRestart = true;
    
    
    /**
     * How long a session must be idle before it is backed up.
     * -1 means sessions won't be backed up.
     */
    private int maxIdleBackup = -1;
    
    
    /**
     * Minimum time a session must be idle before it is swapped to disk.
     * This overrides maxActiveSessions, to prevent thrashing if there are lots
     * of active sessions. Setting to -1 means it's ignored.
     */
    private int minIdleSwap = -1;

    /**
     * The maximum time a session may be idle before it should be swapped
     * to file just on general principle. Setting this to -1 means sessions
     * should not be forced out.
     */
    private int maxIdleSwap = -1;

    /**
     * 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);

    }


    /**
     * Indicates whether sessions are saved when the Manager is shut down
     * properly. This requires the unload() method to be called.
     */
    public boolean getSaveOnRestart() {

        return saveOnRestart;

    }
    
    
    /**
     * Set the option to save sessions to the Store when the Manager is
     * shut down, then loaded when the Manager starts again. If set to 
     * false, any sessions found in the Store may still be picked up when 
     * the Manager is started again.
     *
     * @param save true if sessions should be saved on restart, false if
     *     they should be ignored.
     */
    public void setSaveOnRestart(boolean saveOnRestart) {

        if (saveOnRestart == this.saveOnRestart)
            return;

        boolean oldSaveOnRestart = this.saveOnRestart;
        this.saveOnRestart = saveOnRestart;
        support.firePropertyChange("saveOnRestart",
                                   new Boolean(oldSaveOnRestart),
                                   new Boolean(this.saveOnRestart));
                                   
    }
    
    
    /**
     * 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) {

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

    }


    /**
     * The minimum time in seconds that a session must be idle before
     * it can be swapped out of memory, or -1 if it can be swapped out
     * at any time.
     */
    public int getMinIdleSwap() {

        return minIdleSwap;

    }
    
    
    /**
     * Sets the minimum time in seconds that a session must be idle before
     * it can be swapped out of memory due to maxActiveSession. Set it to -1 
     * if it can be swapped out at any time.
     */
    public void setMinIdleSwap(int min) {

        if (this.minIdleSwap == min)
            return;
        int oldMinIdleSwap = this.minIdleSwap;
        this.minIdleSwap = min;
        support.firePropertyChange("minIdleSwap",
                                   new Integer(oldMinIdleSwap),
                                   new Integer(this.minIdleSwap));

    }
    
    
    /**
     * The time in seconds after which a session should be swapped out of
     * memory to disk.
     */
    public int getMaxIdleSwap() {

        return maxIdleSwap;

    }
    
    
    /**
     * Sets the time in seconds after which a session should be swapped out of
     * memory to disk.
     */
    public void setMaxIdleSwap(int max) {

        if (max == this.maxIdleSwap)
            return;
        int oldMaxIdleSwap = this.maxIdleSwap;
        this.maxIdleSwap = max;
        support.firePropertyChange("maxIdleSwap",
                                   new Integer(oldMaxIdleSwap),
                                   new Integer(this.maxIdleSwap));

    }

    /**
     * Indicates whether sessions will be written to the store after
     * every request in which they are accessed.
     */
    public int getMaxIdleBackup() {

        return maxIdleBackup;

    }
    
    
    /**
     * Sets the option to save sessions to the Store. Setting this to 
     * true means most sessions should be recovered after the server
     * goes down, even if it's not a graceful shutdown.
     */
    public void setMaxIdleBackup (int backup) {

        if (backup == this.maxIdleBackup)
            return;
        int oldBackup = this.maxIdleBackup;
        this.maxIdleBackup = backup;
        support.firePropertyChange("maxIdleBackup",
                                   new Integer(oldBackup),
                                   new Integer(this.maxIdleBackup));
                                   
    }
    
    
    // --------------------------------------------------------- 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);
        if (session != null)
            return (session);

        // See if the Session is in the Store
        if (maxActiveSessions >= 0 || maxIdleSwap >= 0 || 
                maxIdleBackup >= 0)
            session = swapIn(id);

        if (session != null)
            session.access();
        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 {

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

        if (store == null)
            return;

        // If persistence is off, clear the Store and finish
        if (!saveOnRestart && maxActiveSessions < 0 && 
                maxIdleSwap < 0 && maxIdleBackup < 0) {
            try {
                store.clear();
            } catch (IOException e) {
                log("Exception clearing the Store: " + e);
                e.printStackTrace();
            }
            
            return;
        }

        String[] ids = store.keys();
        int n = ids.length;
        if (n == 0)
            return;
            
        if (debug >= 1)
            log(sm.getString("persistentManager.loading", String.valueOf(n)));

        for (int i = 0; i < n; i++)
            swapIn(ids[i]);
        
    }

    /**
     * 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 {

        if (store == null || !saveOnRestart)
            return;

        Session sessions[] = findSessions();
        int n = sessions.length;
        if (n == 0)
            return;
            
        if (debug >= 1)
            log(sm.getString("persistentManager.unloading", String.valueOf(n)));

        for (int i = 0; i < n; i++)
            swapOut(sessions[i]);

    }


    // ------------------------------------------------------ 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() {

        if (!started)
            return;
            
        long timeNow = System.currentTimeMillis();
        Session sessions[] = findSessions();

        for (int i = 0; i < sessions.length; i++) {
            StandardSession session = (StandardSession) sessions[i];
            if (!session.isValid())
                continue;
            if (isSessionStale(session, timeNow))
                session.expire();
        }
    }


    /**
     * Swap idle sessions out to Store if they are idle too long.
     */
    private void processMaxIdleSwaps() {

        if (!started || maxIdleSwap < 0)
            return;

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

        // Swap out all sessions idle longer than maxIdleSwap
        // FIXME: What's preventing us from mangling a session during
        // a request?
        if (maxIdleSwap >= 0) {
            for (int i = 0; i < sessions.length; i++) {
                StandardSession session = (StandardSession) sessions[i];
                if (!session.isValid())
                    continue;
                int timeIdle = // Truncate, do not round up
                    (int) ((timeNow - session.getLastAccessedTime()) / 1000L);
                if (timeIdle > maxIdleSwap && timeIdle > minIdleSwap) {
                    if (debug > 1)
                        log(sm.getString("persistentManager.swapMaxIdle", 
session.getId(), new Integer(timeIdle)));
                    swapOut(session);
                }
            }
        }
        
    }


    /**
     * Swap idle sessions out to Store if too many are active
     */
    private void processMaxActiveSwaps() {

        if (!started || maxActiveSessions < 0)
            return;

        Session sessions[] = findSessions();
        
        // FIXME: Smarter algorithm (LRU)
        if (maxActiveSessions >= sessions.length)
            return;

        if(debug > 0)
            log(sm.getString("persistentManager.tooManyActive", new 
Integer(sessions.length)));

        int toswap = sessions.length - maxActiveSessions;
        long timeNow = System.currentTimeMillis();
        
        for (int i = 0; i < sessions.length && toswap > 0; i++) {
            int timeIdle = // Truncate, do not round up
                (int) ((timeNow - sessions[i].getLastAccessedTime()) / 1000L);
            if (timeIdle > minIdleSwap) {
                if(debug > 1)
                    log(sm.getString("persistentManager.swapTooManyActive", 
sessions[i].getId(), new Integer(timeIdle)));
                swapOut(sessions[i]);
                toswap--;
            }
        }

    }


    /**
     * Back up idle sessions.
     */
    private void processMaxIdleBackups() {

        if (!started || maxIdleBackup < 0)
            return;
        
        // FIXME: Do something useful
    }


    /**
     * Remove the session from the Manager's list of active 
     * sessions and write it out to the Store. 
     *
     * @param session The Session to write out.
     */
    private void swapOut(Session session) {
        
        if (!session.isValid() || isSessionStale(session, System.currentTimeMillis()))
            return;

        ((StandardSession)session).passivate();

        try {
            store.save(session);
        } catch (IOException e) {
            log(sm.getString("persistentManager.serializeError", session.getId(), e));
            throw new IllegalStateException 
(sm.getString("persistentManager.serializeError", session.getId(), e));
        }

        remove((StandardSession)session);
        ((StandardSession)session).recycle();
        recycle((StandardSession)session);

    }


    /**
     * Look for a session in the Store and, if found, restore
     * it in the Manager's list of active sessions if appropriate.
     * The session will be removed from the Store after swapping
     * in, but will not be added to the active session list if it
     * is invalid or past its expiration.
     */
    private Session swapIn(String id) throws IOException {
    
        StandardSession session = null;
        try {
            session = (StandardSession)store.load(id);
        } catch (ClassNotFoundException e) {
            log(sm.getString("persistentManager.deserializeError", id, e));
            throw new IllegalStateException 
(sm.getString("persistentManager.deserializeError", id, e));
        }

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

        if (!session.isValid() || isSessionStale(session, System.currentTimeMillis())) 
{
            log("session swapped in is invalid or expired");
            session.expire();
            store.remove(id);
            return (null);
        }

        if(debug > 2)
            log(sm.getString("persistentManager.swapIn", id));

        session.setManager(this);
        add(session);
        session.activate();

        return (session);
    
    }
    
    
    /**
     * Write the session out to Store, but leave the copy in
     * the Manager's memory unmodified.
     */
    private void backup() throws IOException {
    
        // FIXME: Do something
    
    }


    /**
     * Read the session in from Store, overriding the copy in
     * the Manager's memory.
     */
    private void recover() throws IOException {
    
        // FIXME: Do something
    
    }


    /**
     * Indicate whether the session has been idle for longer
     * than its expiration date as of the supplied time.
     *
     * FIXME: Probably belongs in the Session class.
     */
    private boolean isSessionStale(Session session, long timeNow) {

        int maxInactiveInterval = session.getMaxInactiveInterval();
        if (maxInactiveInterval >= 0) {
            int timeIdle = // Truncate, do not round up
                (int) ((timeNow - session.getLastAccessedTime()) / 1000L);
            if (timeIdle >= maxInactiveInterval)
                return true;
        }
        
        return false;

    }
    
    
    /**
     * 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();
            processMaxIdleSwaps();
            processMaxActiveSwaps();
            processMaxIdleBackups();
        }
    }


}

---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, email: [EMAIL PROTECTED]

Reply via email to