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