"Craig R. McClanahan" wrote: > > [...] > > - Would it be possible to flesh out the rest of the JavaDoc comments? > I would like us to maintain the high quality level of JavaDocs that > Tomcat 4 is known for :-) Done. > - Would it be possible to parameterize the SQL statements used to > access the database? The idea would be that we can adapt to different > table and column names (like JDBCRealm does on the authentication side). Attatched is a slightly refactored JDBCStore with a "stub implementation" of a Connection pooled implementation. However before I change to much regarding specifying columns for the table I want to have som input on the following things. As it is implemented now everything is saved as a Blob so if you wan't to check if the session is invalid or is expired you have to retrive all data and iterate through every blob and make them into a session and THEN see if they are valid or not. I would like to propose that we save additional data in the database. The table would then look something like following: TABLE: [int ID] The ID for this session [boolean ISVALID] True if this session is valid [int MAXINACTIVE] The Max inactive attribute [Blob SESSION] The session object Then you could have a StoredProcedure if you want to that checks for timedout sessions and delete/invalidate them. The StoreProcedure way of doing is of course not the default behavior. I can now check every session for validity in the select query without having to retrieve the data. Thoughts? ..bip
/* * 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; /** * 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 class JDBCStore implements Lifecycle, Runnable, Store { // ----------------------------------------------------- Instance Variables /** * The interval (in seconds) between checks for expired sessions. */ protected int checkInterval = 60; /** * The descriptive information about this implementation. */ protected static String info = "JDBCStore/1.0"; /** * The lifecycle event support for this component. */ protected LifecycleSupport lifecycle = new LifecycleSupport(this); /** * Has this component been started yet? */ protected boolean started = false; /** * The property change support for this component. */ protected PropertyChangeSupport support = new PropertyChangeSupport(this); /** * The string manager for this package. */ protected StringManager sm = StringManager.getManager(Constants.Package); /** * The background thread. */ protected Thread thread = null; /** * The background thread completion semaphore. */ protected boolean threadDone = false; /** * The Manager with which this JDBCStore is associated. */ protected Manager manager; /** * The debugging detail level for this component. */ protected int debug = 0; /** * Name to register for the background thread. */ protected String threadName = "JDBCStore"; /** * Connection string to use when connecting to the DB. */ protected String connString = null; /** * Table to use. */ protected String sessionTable = null; /** * The database connection. */ private Connection conn = null; /** * Driver to use. */ protected String driverName = null; // ------------------------------------------------------------- SQL Variables /** * Variable to hold the <code>getSize()</code> prepared statement. */ protected PreparedStatement preparedSizeSql = null; /** * Variable to hold the <code>keys()</code> prepared statement. */ protected PreparedStatement preparedKeysSql = null; /** * Variable to hold the <code>save()</code> prepared statement. */ protected PreparedStatement preparedSaveSql = null; /** * Variable to hold the <code>clear()</code> prepared statement. */ protected PreparedStatement preparedClearSql = null; /** * Variable to hold the <code>remove()</code> prepared statement. */ protected PreparedStatement preparedRemoveSql = null; /** * Variable to hold the <code>load()</code> prepared statement. */ protected 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"; Connection _conn = getConnection(); ResultSet rst = null; String keys[] = null; int i; if(_conn == null) 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) { ; } release(_conn); _conn = null; } return(keys); } /** * Return an integer containing a count of all Sessions * currently saved in this Store. If there are no Sessions, * <code>0</code> is returned. * * @exception IOException if an input/output error occurred */ public int getSize() throws IOException { int size = 0; String sizeSql = "SELECT COUNT(id) FROM ".concat(sessionTable); Connection _conn = getConnection(); ResultSet rst = null; if(_conn == null) 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) { ; } release(_conn); _conn = null; } return(size); } /** * Load the Session associated with the id <code>id</code>. * If no such session is found <code>null</code> is returned. * * @param id a value of type <code>String</code> * @return the stored <code>Session</code> * @exception ClassNotFoundException if an error occurs * @exception IOException if an input/output error occurred */ public Session load(String id) throws ClassNotFoundException, IOException { ResultSet rst = null; Connection _conn = getConnection(); 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(_conn == null) 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) { ; } release(_conn); _conn = null; } 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 { Connection _conn = getConnection(); String removeSql = ("DELETE FROM ".concat(sessionTable)).concat(" WHERE id = ?"); if(_conn == null) return; try { if(preparedRemoveSql == null) preparedRemoveSql = _conn.prepareStatement(removeSql); preparedRemoveSql.setString(1, id); preparedRemoveSql.execute(); } catch(SQLException e) { log(sm.getString("JDBCStore.SQLException", e)); } finally { release(_conn); _conn = null; } 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 { Connection _conn = getConnection(); String clearSql = "DELETE FROM ".concat(sessionTable); if(_conn == null) return; try { if(preparedClearSql == null) preparedClearSql = _conn.prepareStatement(clearSql); preparedClearSql.execute(); } catch(SQLException e) { log(sm.getString("JDBCStore.SQLException", e)); } finally { release(_conn); _conn = null; } } /** * Save a session to the Store. * * @param session the session to be stored * @exception IOException if an input/output error occurs */ public void save(Session session) throws IOException { String saveSql = ("INSERT INTO ".concat(sessionTable)).concat(" VALUES (?, ?)"); Connection _conn = getConnection(); ObjectOutputStream oos = null; ByteArrayOutputStream bos = null; ByteArrayInputStream bis = null; InputStream in = null; if(_conn == null) 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; release(_conn); _conn = null; } if (debug > 0) log(sm.getString("JDBCStore.saving", session.getId(), sessionTable)); } // --------------------------------------------------------- Protected Methods /** * Called by our background reaper thread to check if Sessions * saved in our store are subject of being expired. If so expire * the Session and remove it from the Store. * */ protected 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(); } } } /** * Check the connection associated with this store, if it's * <code>null</code> or closed try to reopen it. * Returns <code>null</code> if the connection could not be established. * * @return <code>Connection</code> if the connection suceeded */ protected Connection getConnection(){ 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")); } } catch (SQLException ex){ log(sm.getString("JDBCStore.checkConnectionSQLException", ex.toString())); } catch (ClassNotFoundException ex) { log(sm.getString("JDBCStore.checkConnectionClassNotFoundException", ex.toString())); } return conn; } /** * Release the connection, not needed here since the * connection is not associated with a connection pool. * * @param conn The connection to be released */ protected void release(Connection conn) { ; } /** * Log a message on the Logger associated with our Container (if any). * * @param message Message to be logged */ protected 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 this.conn = getConnection(); } /** * 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 and release everything associated with our db. if(conn != null) { try { conn.commit(); } catch (SQLException e) { ; } try { preparedSizeSql.close(); } catch (SQLException e) { ; } try { preparedKeysSql.close(); } catch (SQLException e) { ; } try { preparedSaveSql.close(); } catch (SQLException e) { ; } try { preparedClearSql.close(); } catch (SQLException e) { ; } try { preparedRemoveSql.close(); } catch (SQLException e) { ; } try { preparedLoadSql.close(); } catch (SQLException e) { ; } try { conn.close(); } catch (SQLException e) { ; } this.preparedSizeSql = null; this.preparedKeysSql = null; this.preparedSaveSql = null; this.preparedClearSql = null; this.preparedRemoveSql = null; this.preparedLoadSql = null; this.conn = null; } } /** * Start the background thread that will periodically check for * session timeouts. */ protected 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. */ protected void threadSleep() { try { Thread.sleep(checkInterval * 1000L); } catch (InterruptedException e) { ; } } /** * Stop the background thread that is periodically checking for * session timeouts. */ protected void threadStop() { if (thread == null) return; threadDone = true; thread.interrupt(); try { thread.join(); } catch (InterruptedException e) { ; } thread = null; } }
/* * JDBCPooledStore.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.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; /** * Extension to JDBCStore which implements JDBC using a pool * of connections. * * @author Bip Thelin * @version $Revision$, $Date$ */ public final class JDBCPooledStore extends JDBCStore{ // ----------------------------------------------------- Instance Variables /** * The descriptive information about this implementation. */ protected static final String info = "JDBCPooledStore/1.0"; /** * Name to register for the background thread. */ protected String threadName = "JDBCPooledStore"; // --------------------------------------------------------- Protected Methods /** * Check the connection associated with this store, if it's * <code>null</code> or closed try to reopen it. * Returns <code>null</code> if the connection could not be established. * Overrides JDBCStore#checkConnection and uses a connection pool. * * @return <code>Connection</code> if the connection suceeded * @see JDBCStore */ protected Connection getConnection(){ return null; // Implement connection pool } /** * Release the connection, and give it back to the pool * * @param conn The connection to be released */ protected void release(Connection conn) { ; } }