Here's the JDBCStore implementation. To use it change your server.xml to something 
like:

<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.JDBCStore"                       
         driverName="com.merant.datadirect.jdbc.sqlserver.SQLServerDriver"
                sessionTable="tomcat$sessions"
                
connectionURL="jdbc:mysql://localhost/authority?user=test;password=test"
                debug="99"/>
</Manager>

You also have to create a table that has the fields id, session. And where id is a 
varchar field
and session is a binary field, i.e. Blob. Sort of like:
CREATE TABLE [dbo].[tomcat$sessions] (
        [id] [varchar] (50) NOT NULL ,
        [session] [binary] (1000) NULL 
)

However the SQL command varies from different RDBMS.

        ..bip

-----------------------------------------------------------------------------------------
Index: LocalStrings.properties
===================================================================
RCS file: 
/home/cvspublic/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/session/LocalStrings.properties,v
retrieving revision 1.4
diff -u -r1.4 LocalStrings.properties
--- LocalStrings.properties     2001/02/03 20:36:20     1.4
+++ LocalStrings.properties     2001/04/08 01:57:06
@@ -5,6 +5,16 @@
 fileStore.saving=Saving Session {0} to file {1}
 fileStore.loading=Loading Session {0} from file {1}
 fileStore.removing=Removing Session {0} at file {1}
+JDBCStore.alreadyStarted=JDBC Store has already been started
+JDBCStore.notStarted=JDBC Store has not yet been started
+JDBCStore.saving=Saving Session {0} to database {1}
+JDBCStore.loading=Loading Session {0} from database {1}
+JDBCStore.removing=Removing Session {0} at database {1}
+JDBCStore.SQLException=SQL Error {0}
+JDBCStore.checkConnectionDBClosed=The database connection is null or was found
to be closed. Trying to re-open it.
+JDBCStore.checkConnectionDBReOpenFail=The re-open on the database failed. The d
atabase could be down.
+JDBCStore.checkConnectionSQLException=A SQL exception occured {0}
+JDBCStore.checkConnectionClassNotFoundException=JDBC driver class not found {0}

 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




/*
 * JDBCStore.java
 * $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.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.catalina.Container;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
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.util.CustomObjectInputStream;
import org.apache.catalina.util.LifecycleSupport;
import org.apache.catalina.util.StringManager;

/**
 * Concrete implementation of the <code>Store</code> interface that stores
 * serialized session objects in a database.  Sessions that are
 * saved are still subject to being expired based on inactivity.
 *
 * @author Bip Thelin
 * @version $Revision$, $Date$
 */

public final class JDBCStore
    implements Lifecycle, Runnable, Store {

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

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

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

    /**
     * The property change support for this component.
     */
    private PropertyChangeSupport support = new PropertyChangeSupport(this);

    /**
     * The string manager for this package.
     */
    private StringManager sm = StringManager.getManager(Constants.Package);

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

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

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

    /**
     * Connection string to use when connecting to the DB.
     */
    private String connString = null;

    /**
     * Table to use.
     */
    private String sessionTable = null;

    /**
     * The database connection.
     */
    private Connection conn = null;

    /**
     * Driver to use.
     */
    private String driverName = null;

    // ------------------------------------------------------------- SQL Variables

    private PreparedStatement preparedSizeSql = null;

    private PreparedStatement preparedKeysSql = null;

    private PreparedStatement preparedSaveSql = null;

    private PreparedStatement preparedClearSql = null;

    private PreparedStatement preparedRemoveSql = null;

    private PreparedStatement preparedLoadSql = null;

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

    /**
     * Return the info for this Store.
     */
    public String getInfo() {
        return(info);
    }

    /**
     * Set the debugging detail level for this Store.
     *
     * @param debug The new debugging detail level
     */
    public void setDebug(int debug) {
        this.debug = debug;
    }

    /**
     * Return the debugging detail level for this Store.
     */
    public int getDebug() {
        return(this.debug);
    }

    /**
     * Set the driver for this Store.
     *
     * @param driverName The new driver
     */
    public void setDriverName(String driverName) {
        String oldDriverName = this.driverName;
        this.driverName = driverName;
        support.firePropertyChange("driverName",
                                   oldDriverName,
                                   this.driverName);
        this.driverName = driverName;
    }

    /**
     * Return the driver for this Store.
     */
    public String getDriverName() {
        return(this.driverName);
    }

    /**
     * Set the Connection URL for this Store.
     *
     * @param connectionURL The new Connection URL
     */
    public void setConnectionURL(String connectionURL) {
        String oldConnString = this.connString;
        this.connString = connectionURL;
        support.firePropertyChange("connString",
                                   oldConnString,
                                   this.connString);
    }

    /**
     * Return the Connection URL for this Store.
     */
    public String getConnectionURL() {
        return(this.connString);
    }

    /**
     * Set the table for this Store.
     *
     * @param sessionTable The new table
     */
    public void setSessionTable(String sessionTable) {
        String oldSessionTable = this.sessionTable;
        this.sessionTable = sessionTable;
        support.firePropertyChange("sessionTable",
                                   oldSessionTable,
                                   this.sessionTable);
    }

    /**
     * Return the table for this Store.
     */
    public String getSessionTable() {
        return(this.driverName);
    }

    /**
     * Set the check interval (in seconds) for this Store.
     *
     * @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));
    }

    /**
     * Return the check interval (in seconds) for this Store.
     */
    public int getCheckInterval() {
        return(this.checkInterval);
    }

    /**
     * Set the Manager with which this JDBCStore 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 Manager with which the JDBCStore is associated.
     */
    public Manager getManager() {
        return(this.manager);
    }

    // --------------------------------------------------------- Public 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 add
     */
    public void removeLifecycleListener(LifecycleListener listener) {
        lifecycle.removeLifecycleListener(listener);
    }

    /**
     * Add a property change listener to this component.
     *
     * @param listener a value of type 'PropertyChangeListener'
     */
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        support.addPropertyChangeListener(listener);
    }

    /**
     * Remove a property change listener from this component.
     *
     * @param listener The listener to remove
     */
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        support.removePropertyChangeListener(listener);
    }

    /**
     * Return an array containing the session identifiers of all Sessions
     * currently saved in this Store.  If there are no such Sessions, a
     * zero-length array is returned.
     *
     * @exception IOException if an input/output error occurred
     */
    public String[] keys() throws IOException {
        String keysSql =
            "SELECT c.size, s.id FROM "+sessionTable+" s, "+
            "(SELECT COUNT(id) AS size FROM "+sessionTable+") c";
        ResultSet rst = null;
        String keys[] = null;
        int i;

        if(!checkConnection())
            return(new String[0]);

        try {
            if(preparedKeysSql == null)
                preparedKeysSql = conn.prepareStatement(keysSql);

            rst = preparedKeysSql.executeQuery();
            if (rst != null && rst.next()) {
                keys = new String[rst.getInt(1)];
                keys[0] = rst.getString(2);
                i=1;

                while(rst.next())
                    keys[i++] = rst.getString(2);
            } else {
                keys = new String[0];
            }
        } catch(SQLException e) {
            log(sm.getString("JDBCStore.SQLException", e));
        } finally {
            try {
                if(rst != null)
                    rst.close();
            } catch(SQLException e) {
                ;
            }
        }       

        return(keys);
    }

    public int getSize() throws IOException {
        int size = 0;
        String sizeSql = "SELECT COUNT(id) FROM ".concat(sessionTable);
        ResultSet rst = null;

        if(!checkConnection())
            return(size);

        try {
            if(preparedSizeSql == null)
                preparedSizeSql = conn.prepareStatement(sizeSql);

            rst = preparedSizeSql.executeQuery();
            if (rst.next())
                size = rst.getInt(1);
        } catch(SQLException e) {
            log(sm.getString("JDBCStore.SQLException", e));
        } finally {
            try {
                if(rst != null)
                    rst.close();
            } catch(SQLException e) {
                ;
            }
        }

        return(size);
    }

    public Session load(String id)
        throws ClassNotFoundException, IOException {
        ResultSet rst = null;
        StandardSession _session = null;
        Loader loader = null;
        ClassLoader classLoader = null;
        ObjectInputStream ois = null;
        BufferedInputStream bis = null;
        Container container = manager.getContainer();
        String loadSql =
            ("SELECT id, session FROM ".concat(sessionTable)).concat(" WHERE id = ?");

        if(!checkConnection())
            return(null);

        try {
            if(preparedLoadSql == null)
                preparedLoadSql = conn.prepareStatement(loadSql);

            preparedLoadSql.setString(1, id);
            rst = preparedLoadSql.executeQuery();
            if (rst.next()) {
                bis = new BufferedInputStream(rst.getBinaryStream(2));

                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);
            } else if (debug > 0) {
                    log("JDBCStore: No persisted data object found");
            }
        } catch(SQLException e) {
            log(sm.getString("JDBCStore.SQLException", e));
        } finally {
            try {
                if(rst != null)
                    rst.close();
            } catch(SQLException e) {
                ;
            }
        }

        try {
            _session = (StandardSession) manager.createSession();
            _session.readObjectData(ois);
            _session.setManager(manager);
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                    bis = null;
                } catch (IOException e) {
                    ;
                }
            }
        }

        if (debug > 0)
            log(sm.getString("JDBCStore.loading",
                             id, sessionTable));
        return(_session);
    }

    /**
     * Remove the Session with the specified session identifier from
     * this Store, if present.  If no such Session is present, this method
     * takes no action.
     *
     * @param id Session identifier of the Session to be removed
     *
     * @exception IOException if an input/output error occurs
     */
    public void remove(String id) throws IOException {
        String removeSql =
            ("DELETE FROM ".concat(sessionTable)).concat(" WHERE id = ?");

        if(!checkConnection())
            return;

        try {
            if(preparedRemoveSql == null)
                preparedRemoveSql = conn.prepareStatement(removeSql);

            preparedRemoveSql.setString(1, id);
            preparedRemoveSql.execute();
        } catch(SQLException e) {
            log(sm.getString("JDBCStore.SQLException", e));
        }

        if (debug > 0)
            log(sm.getString("JDBCStore.removing", id, sessionTable));
    }

    /**
     * Remove all of the Sessions in this Store.
     *
     * @exception IOException if an input/output error occurs
     */
    public void clear() throws IOException {
        String clearSql = "DELETE FROM ".concat(sessionTable);

        if(!checkConnection())
            return;

        try {
            if(preparedClearSql == null)
                preparedClearSql = conn.prepareStatement(clearSql);

            preparedClearSql.execute();
        } catch(SQLException e) {
            log(sm.getString("JDBCStore.SQLException", e));
        }
    }

    public void save(Session session) throws IOException {
        String saveSql =
            ("INSERT INTO ".concat(sessionTable)).concat(" VALUES (?, ?)");
        ObjectOutputStream oos = null;
        ByteArrayOutputStream bos = null;
        ByteArrayInputStream bis = null;
        InputStream in = null;

        if(!checkConnection())
            return;

        // If sessions already exist in DB, remove and insert again.
        // TODO:
        // * Check if ID exists in database and if so use UPDATE.
        remove(session.getId());

        try {
            bos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(new BufferedOutputStream(bos));

            ((StandardSession)session).writeObjectData(oos);
            oos.close();

            byte[] obs = bos.toByteArray();
            int size = obs.length;
            bis = new ByteArrayInputStream(obs, 0, size);
            in = new BufferedInputStream(bis, size);
            
            if(preparedSaveSql == null)
                preparedSaveSql = conn.prepareStatement(saveSql);

            preparedSaveSql.setString(1, session.getId());
            preparedSaveSql.setBinaryStream(2, in, size);
            preparedSaveSql.execute();
        } catch(SQLException e) {
            log(sm.getString("JDBCStore.SQLException", e));
        } catch (IOException e) {
            ;
        } finally {
            if(bis != null)
                bis.close();

            if(in != null)
                in.close();

            bis = null;
            bos = null;
            oos = null;
            in = null;
        }
        if (debug > 0)
            log(sm.getString("JDBCStore.saving",
                             session.getId(), sessionTable));
    }

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

    private void processExpires() {
        long timeNow = System.currentTimeMillis();
        String[] keys = null;

        if(!started)
            return;
        
        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();
                    remove(session.getId());
                }
            } catch (IOException e) {
                log (e.toString());
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                log (e.toString());
                e.printStackTrace();
            }
        }
    }

    private boolean checkConnection(){
        boolean state = false;

        try {
            if(conn == null || conn.isClosed()) {
                Class.forName(driverName);
                log(sm.getString("JDBCStore.checkConnectionDBClosed"));
                conn = DriverManager.getConnection(connString);
                conn.setAutoCommit(true);

                if(conn == null || conn.isClosed()) {
                  log(sm.getString("JDBCStore.checkConnectionDBReOpenFail"));
                  state = false;
                } else {
                    state = true;
                }
            } else {
                state = true;
            }
        } catch (SQLException ex){
            log(sm.getString("JDBCStore.checkConnectionSQLException",
                             ex.toString()));
        } catch (ClassNotFoundException ex) {
            log(sm.getString("JDBCStore.checkConnectionClassNotFoundException",
                             ex.toString()));
        }

        return state;
    }

    /**
     * Log a message on the Logger associated with our Container (if any).
     *
     * @param message Message to be logged
     */
    private 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);
        }
    }

    // --------------------------------------------------------- Thread Methods

    /**
     * The background thread that checks for session timeouts and shutdown.
     */
    public void run() {
        // Loop until the termination semaphore is set
        while (!threadDone) {
            threadSleep();
            processExpires();
        }
    }

    /**
     * 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 {
        // Validate and update our current component state
        if (started)
            throw new LifecycleException
                (sm.getString("JDBCStore.alreadyStarted"));
        lifecycle.fireLifecycleEvent(START_EVENT, null);
        started = true;

        // Start the background reaper thread
        threadStart();

        // Open connection to the database
        checkConnection();
    }

    /**
     * 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 {
        // Validate and update our current component state
        if (!started)
            throw new LifecycleException
                (sm.getString("JDBCStore.notStarted"));
        lifecycle.fireLifecycleEvent(STOP_EVENT, null);
        started = false;

        // Stop the background reaper thread
        threadStop();

        // Close the connection to the database.
        if(conn != null) {
            try {
                conn.commit();
                conn.close();
            } catch (SQLException e) {
                ;
            }
        }
    }

    /**
     * Start the background thread that will periodically check for
     * session timeouts.
     */
    private void threadStart() {
        if (thread != null)
            return;

        threadDone = false;
        thread = new Thread(this, threadName);
        thread.setDaemon(true);
        thread.start();
    }

    /**
     * Sleep for the duration specified by the <code>checkInterval</code>
     * property.
     */
    private void threadSleep() {
        try {
            Thread.sleep(checkInterval * 1000L);
        } catch (InterruptedException e) {
            ;
        }
    }

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

Reply via email to