]> granicus.if.org Git - postgresql/commitdiff
Added DataSource code and tests submitted by Aaron Mulder
authorDave Cramer <davec@fastcrypt.com>
Tue, 30 Jul 2002 11:41:10 +0000 (11:41 +0000)
committerDave Cramer <davec@fastcrypt.com>
Tue, 30 Jul 2002 11:41:10 +0000 (11:41 +0000)
src/interfaces/jdbc/org/postgresql/jdbc2/optional/BaseDataSource.java [new file with mode: 0644]
src/interfaces/jdbc/org/postgresql/jdbc2/optional/ConnectionPool.java [new file with mode: 0644]
src/interfaces/jdbc/org/postgresql/jdbc2/optional/PGObjectFactory.java [new file with mode: 0644]
src/interfaces/jdbc/org/postgresql/jdbc2/optional/PooledConnectionImpl.java [new file with mode: 0644]
src/interfaces/jdbc/org/postgresql/jdbc2/optional/PoolingDataSource.java [new file with mode: 0644]
src/interfaces/jdbc/org/postgresql/jdbc2/optional/SimpleDataSource.java [new file with mode: 0644]
src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/BaseDataSourceTest.java [new file with mode: 0644]
src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/ConnectionPoolTest.java [new file with mode: 0644]
src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/OptionalTestSuite.java [new file with mode: 0644]
src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/SimpleDataSourceTest.java [new file with mode: 0644]

diff --git a/src/interfaces/jdbc/org/postgresql/jdbc2/optional/BaseDataSource.java b/src/interfaces/jdbc/org/postgresql/jdbc2/optional/BaseDataSource.java
new file mode 100644 (file)
index 0000000..0e3ae8e
--- /dev/null
@@ -0,0 +1,224 @@
+package org.postgresql.jdbc2.optional;
+
+import javax.naming.*;
+import java.io.PrintWriter;
+import java.sql.*;
+
+/**
+ * Base class for data sources and related classes.
+ *
+ * @author Aaron Mulder (ammulder@chariotsolutions.com)
+ * @version $Revision: 1.1 $
+ */
+public abstract class BaseDataSource implements Referenceable {
+    // Load the normal driver, since we'll use it to actually connect to the
+    // database.  That way we don't have to maintain the connecting code in
+    // multiple places.
+    static {
+        try {
+            Class.forName("org.postgresql.Driver");
+        } catch (ClassNotFoundException e) {
+            System.err.println("PostgreSQL DataSource unable to load PostgreSQL JDBC Driver");
+        }
+    }
+
+    // Needed to implement the DataSource/ConnectionPoolDataSource interfaces
+    private transient PrintWriter logger;
+    // Don't track loginTimeout, since we'd just ignore it anyway
+
+    // Standard properties, defined in the JDBC 2.0 Optional Package spec
+    private String serverName = "localhost";
+    private String databaseName;
+    private String user;
+    private String password;
+    private int portNumber;
+
+    /**
+     * Gets a connection to the PostgreSQL database.  The database is identified by the
+     * DataSource properties serverName, databaseName, and portNumber.  The user to
+     * connect as is identified by the DataSource properties user and password.
+     *
+     * @return A valid database connection.
+     * @throws SQLException
+     *         Occurs when the database connection cannot be established.
+     */
+    public Connection getConnection() throws SQLException {
+        return getConnection(user, password);
+    }
+
+    /**
+     * Gets a connection to the PostgreSQL database.  The database is identified by the
+     * DataAource properties serverName, databaseName, and portNumber.  The user to
+     * connect as is identified by the arguments user and password, which override
+     * the DataSource properties by the same name.
+     *
+     * @return A valid database connection.
+     * @throws SQLException
+     *         Occurs when the database connection cannot be established.
+     */
+    public Connection getConnection(String user, String password) throws SQLException {
+        try {
+            Connection con = DriverManager.getConnection(getUrl(), user, password);
+            if (logger != null) {
+                logger.println("Created a non-pooled connection for " + user + " at " + getUrl());
+            }
+            return con;
+        } catch (SQLException e) {
+            if (logger != null) {
+                logger.println("Failed to create a non-pooled connection for " + user + " at " + getUrl() + ": " + e);
+            }
+            throw e;
+        }
+    }
+
+    /**
+     * This DataSource does not support a configurable login timeout.
+     * @return 0
+     */
+    public int getLoginTimeout() throws SQLException {
+        return 0;
+    }
+
+    /**
+     * This DataSource does not support a configurable login timeout.  Any value
+     * provided here will be ignored.
+     */
+    public void setLoginTimeout(int i) throws SQLException {
+    }
+
+    /**
+     * Gets the log writer used to log connections opened.
+     */
+    public PrintWriter getLogWriter() throws SQLException {
+        return logger;
+    }
+
+    /**
+     * The DataSource will note every connection opened to the provided log writer.
+     */
+    public void setLogWriter(PrintWriter printWriter) throws SQLException {
+        logger = printWriter;
+    }
+
+    /**
+     * Gets the name of the host the PostgreSQL database is running on.
+     */
+    public String getServerName() {
+        return serverName;
+    }
+
+    /**
+     * Sets the name of the host the PostgreSQL database is running on.  If this
+     * is changed, it will only affect future calls to getConnection.  The default
+     * value is <tt>localhost</tt>.
+     */
+    public void setServerName(String serverName) {
+        if(serverName == null || serverName.equals("")) {
+            this.serverName = "localhost";
+        } else {
+            this.serverName = serverName;
+        }
+    }
+
+    /**
+     * Gets the name of the PostgreSQL database, running on the server identified
+     * by the serverName property.
+     */
+    public String getDatabaseName() {
+        return databaseName;
+    }
+
+    /**
+     * Sets the name of the PostgreSQL database, running on the server identified
+     * by the serverName property.  If this is changed, it will only affect
+     * future calls to getConnection.
+     */
+    public void setDatabaseName(String databaseName) {
+        this.databaseName = databaseName;
+    }
+
+    /**
+     * Gets a description of this DataSource-ish thing.  Must be customized by
+     * subclasses.
+     */
+    public abstract String getDescription();
+
+    /**
+     * Gets the user to connect as by default.  If this is not specified, you must
+     * use the getConnection method which takes a user and password as parameters.
+     */
+    public String getUser() {
+        return user;
+    }
+
+    /**
+     * Sets the user to connect as by default.  If this is not specified, you must
+     * use the getConnection method which takes a user and password as parameters.
+     * If this is changed, it will only affect future calls to getConnection.
+     */
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    /**
+     * Gets the password to connect with by default.  If this is not specified but a
+     * password is needed to log in, you must use the getConnection method which takes
+     * a user and password as parameters.
+     */
+    public String getPassword() {
+        return password;
+    }
+
+    /**
+     * Sets the password to connect with by default.  If this is not specified but a
+     * password is needed to log in, you must use the getConnection method which takes
+     * a user and password as parameters.  If this is changed, it will only affect
+     * future calls to getConnection.
+     */
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    /**
+     * Gets the port which the PostgreSQL server is listening on for TCP/IP
+     * connections.
+     *
+     * @return The port, or 0 if the default port will be used.
+     */
+    public int getPortNumber() {
+        return portNumber;
+    }
+
+    /**
+     * Gets the port which the PostgreSQL server is listening on for TCP/IP
+     * connections.  Be sure the -i flag is passed to postmaster when PostgreSQL
+     * is started.  If this is not set, or set to 0, the default port will be used.
+     */
+    public void setPortNumber(int portNumber) {
+        this.portNumber = portNumber;
+    }
+
+    /**
+     * Generates a DriverManager URL from the other properties supplied.
+     */
+    private String getUrl() {
+        return "jdbc:postgresql://"+serverName+(portNumber == 0 ? "" : ":"+portNumber)+"/"+databaseName;
+    }
+
+    public Reference getReference() throws NamingException {
+        Reference ref = new Reference(getClass().getName(), PGObjectFactory.class.getName(), null);
+        ref.add(new StringRefAddr("serverName", serverName));
+        if (portNumber != 0) {
+            ref.add(new StringRefAddr("portNumber", Integer.toString(portNumber)));
+        }
+        ref.add(new StringRefAddr("databaseName", databaseName));
+        if (user != null) {
+            ref.add(new StringRefAddr("user", user));
+        }
+        if (password != null) {
+            ref.add(new StringRefAddr("password", password));
+        }
+        return ref;
+    }
+
+}
diff --git a/src/interfaces/jdbc/org/postgresql/jdbc2/optional/ConnectionPool.java b/src/interfaces/jdbc/org/postgresql/jdbc2/optional/ConnectionPool.java
new file mode 100644 (file)
index 0000000..1ffee5d
--- /dev/null
@@ -0,0 +1,76 @@
+package org.postgresql.jdbc2.optional;
+
+import javax.sql.ConnectionPoolDataSource;
+import javax.sql.PooledConnection;
+import java.sql.SQLException;
+import java.io.Serializable;
+
+/**
+ * PostgreSQL implementation of ConnectionPoolDataSource.  The app server or
+ * middleware vendor should provide a DataSource implementation that takes advantage
+ * of this ConnectionPoolDataSource.  If not, you can use the PostgreSQL implementation
+ * known as PoolingDataSource, but that should only be used if your server or middleware
+ * vendor does not provide their own.  Why? The server may want to reuse the same
+ * Connection across all EJBs requesting a Connection within the same Transaction, or
+ * provide other similar advanced features.
+ *
+ * <p>In any case, in order to use this ConnectionPoolDataSource, you must set the property
+ * databaseName.  The settings for serverName, portNumber, user, and password are
+ * optional.  Note: these properties are declared in the superclass.</p>
+ *
+ * <p>This implementation supports JDK 1.3 and higher.</p>
+ *
+ * @author Aaron Mulder (ammulder@chariotsolutions.com)
+ * @version $Revision: 1.1 $
+ */
+public class ConnectionPool extends BaseDataSource implements Serializable, ConnectionPoolDataSource {
+    private boolean defaultAutoCommit = false;
+
+    /**
+     * Gets a description of this DataSource.
+     */
+    public String getDescription() {
+        return "ConnectionPoolDataSource from "+org.postgresql.Driver.getVersion();
+    }
+
+    /**
+     * Gets a connection which may be pooled by the app server or middleware
+     * implementation of DataSource.
+     *
+     * @throws java.sql.SQLException
+     *         Occurs when the physical database connection cannot be established.
+     */
+    public PooledConnection getPooledConnection() throws SQLException {
+        return new PooledConnectionImpl(getConnection(), defaultAutoCommit);
+    }
+
+    /**
+     * Gets a connection which may be pooled by the app server or middleware
+     * implementation of DataSource.
+     *
+     * @throws java.sql.SQLException
+     *         Occurs when the physical database connection cannot be established.
+     */
+    public PooledConnection getPooledConnection(String user, String password) throws SQLException {
+        return new PooledConnectionImpl(getConnection(user, password), defaultAutoCommit);
+    }
+
+    /**
+     * Gets whether connections supplied by this pool will have autoCommit
+     * turned on by default.  The default value is <tt>false</tt>, so that
+     * autoCommit will be turned off by default.
+     */
+    public boolean isDefaultAutoCommit() {
+        return defaultAutoCommit;
+    }
+
+    /**
+     * Sets whether connections supplied by this pool will have autoCommit
+     * turned on by default.  The default value is <tt>false</tt>, so that
+     * autoCommit will be turned off by default.
+     */
+    public void setDefaultAutoCommit(boolean defaultAutoCommit) {
+        this.defaultAutoCommit = defaultAutoCommit;
+    }
+
+}
diff --git a/src/interfaces/jdbc/org/postgresql/jdbc2/optional/PGObjectFactory.java b/src/interfaces/jdbc/org/postgresql/jdbc2/optional/PGObjectFactory.java
new file mode 100644 (file)
index 0000000..0663062
--- /dev/null
@@ -0,0 +1,89 @@
+package org.postgresql.jdbc2.optional;
+
+import javax.naming.spi.ObjectFactory;
+import javax.naming.*;
+import java.util.Hashtable;
+
+/**
+ * Returns a DataSource-ish thing based on a JNDI reference.  In the case of a
+ * SimpleDataSource or ConnectionPool, a new instance is created each time, as
+ * there is no connection state to maintain. In the case of a PoolingDataSource,
+ * the same DataSource will be returned for every invocation within the same
+ * VM/ClassLoader, so that the state of the connections in the pool will be
+ * consistent.
+ *
+ * @author Aaron Mulder (ammulder@chariotsolutions.com)
+ * @version $Revision: 1.1 $
+ */
+public class PGObjectFactory implements ObjectFactory {
+    /**
+     * Dereferences a PostgreSQL DataSource.  Other types of references are
+     * ignored.
+     */
+    public Object getObjectInstance(Object obj, Name name, Context nameCtx,
+                                    Hashtable environment) throws Exception {
+        Reference ref = (Reference)obj;
+        if(ref.getClassName().equals(SimpleDataSource.class.getName())) {
+            return loadSimpleDataSource(ref);
+        } else if (ref.getClassName().equals(ConnectionPool.class.getName())) {
+            return loadConnectionPool(ref);
+        } else if (ref.getClassName().equals(PoolingDataSource.class.getName())) {
+            return loadPoolingDataSource(ref);
+        } else {
+            return null;
+        }
+    }
+
+    private Object loadPoolingDataSource(Reference ref) {
+        // If DataSource exists, return it
+        String name = getProperty(ref, "dataSourceName");
+        PoolingDataSource pds = PoolingDataSource.getDataSource(name);
+        if(pds != null) {
+            return pds;
+        }
+        // Otherwise, create a new one
+        pds = new PoolingDataSource();
+        pds.setDataSourceName(name);
+        loadBaseDataSource(pds, ref);
+        String min = getProperty(ref, "initialConnections");
+        if (min != null) {
+            pds.setInitialConnections(Integer.parseInt(min));
+        }
+        String max = getProperty(ref, "maxConnections");
+        if (max != null) {
+            pds.setMaxConnections(Integer.parseInt(max));
+        }
+        return pds;
+    }
+
+    private Object loadSimpleDataSource(Reference ref) {
+        SimpleDataSource ds = new SimpleDataSource();
+        return loadBaseDataSource(ds, ref);
+    }
+
+    private Object loadConnectionPool(Reference ref) {
+        ConnectionPool cp = new ConnectionPool();
+        return loadBaseDataSource(cp, ref);
+    }
+
+    private Object loadBaseDataSource(BaseDataSource ds, Reference ref) {
+        ds.setDatabaseName(getProperty(ref, "databaseName"));
+        ds.setPassword(getProperty(ref, "password"));
+        String port = getProperty(ref, "portNumber");
+        if(port != null) {
+            ds.setPortNumber(Integer.parseInt(port));
+        }
+        ds.setServerName(getProperty(ref, "serverName"));
+        ds.setUser(getProperty(ref, "user"));
+        return ds;
+    }
+
+    private String getProperty(Reference ref, String s) {
+        RefAddr addr = ref.get(s);
+        if(addr == null) {
+            return null;
+        }
+        return (String)addr.getContent();
+    }
+
+}
diff --git a/src/interfaces/jdbc/org/postgresql/jdbc2/optional/PooledConnectionImpl.java b/src/interfaces/jdbc/org/postgresql/jdbc2/optional/PooledConnectionImpl.java
new file mode 100644 (file)
index 0000000..bbd801e
--- /dev/null
@@ -0,0 +1,199 @@
+package org.postgresql.jdbc2.optional;
+
+import javax.sql.*;
+import java.sql.SQLException;
+import java.sql.Connection;
+import java.util.*;
+import java.lang.reflect.*;
+
+/**
+ * PostgreSQL implementation of the PooledConnection interface.  This shouldn't
+ * be used directly, as the pooling client should just interact with the
+ * ConnectionPool instead.
+ * @see ConnectionPool
+ *
+ * @author Aaron Mulder (ammulder@chariotsolutions.com)
+ * @version $Revision: 1.1 $
+ */
+public class PooledConnectionImpl implements PooledConnection {
+    private List listeners = new LinkedList();
+    private Connection con;
+    private ConnectionHandler last;
+    private boolean autoCommit;
+
+    /**
+     * Creates a new PooledConnection representing the specified physical
+     * connection.
+     */
+    PooledConnectionImpl(Connection con, boolean autoCommit) {
+        this.con = con;
+        this.autoCommit = autoCommit;
+    }
+
+    /**
+     * Adds a listener for close or fatal error events on the connection
+     * handed out to a client.
+     */
+    public void addConnectionEventListener(ConnectionEventListener connectionEventListener) {
+        listeners.add(connectionEventListener);
+    }
+
+    /**
+     * Removes a listener for close or fatal error events on the connection
+     * handed out to a client.
+     */
+    public void removeConnectionEventListener(ConnectionEventListener connectionEventListener) {
+        listeners.remove(connectionEventListener);
+    }
+
+    /**
+     * Closes the physical database connection represented by this
+     * PooledConnection.  If any client has a connection based on
+     * this PooledConnection, it is forcibly closed as well.
+     */
+    public void close() throws SQLException {
+        if(last != null) {
+            last.close();
+            if(!con.getAutoCommit()) {
+                try {con.rollback();} catch (SQLException e) {}
+            }
+        }
+        try {
+            con.close();
+        } finally {
+            con = null;
+        }
+    }
+
+    /**
+     * Gets a handle for a client to use.  This is a wrapper around the
+     * physical connection, so the client can call close and it will just
+     * return the connection to the pool without really closing the
+     * pgysical connection.
+     *
+     * <p>According to the JDBC 2.0 Optional Package spec (6.2.3), only one
+     * client may have an active handle to the connection at a time, so if
+     * there is a previous handle active when this is called, the previous
+     * one is forcibly closed and its work rolled back.</p>
+     */
+    public Connection getConnection() throws SQLException {
+        if(con == null) {
+            throw new SQLException("This PooledConnection has already been closed!");
+        }
+        // Only one connection can be open at a time from this PooledConnection.  See JDBC 2.0 Optional Package spec section 6.2.3
+        if(last != null) {
+            last.close();
+            if(!con.getAutoCommit()) {
+                try {con.rollback();} catch(SQLException e) {}
+            }
+            con.clearWarnings();
+        }
+        con.setAutoCommit(autoCommit);
+        ConnectionHandler handler = new ConnectionHandler(con);
+        last = handler;
+        return (Connection)Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{Connection.class}, handler);
+    }
+
+    /**
+     * Used to fire a connection event to all listeners.
+     */
+    void fireConnectionClosed() {
+        ConnectionEvent evt = null;
+        // Copy the listener list so the listener can remove itself during this method call
+        ConnectionEventListener[] local = (ConnectionEventListener[]) listeners.toArray(new ConnectionEventListener[listeners.size()]);
+        for (int i = 0; i < local.length; i++) {
+            ConnectionEventListener listener = local[i];
+            if (evt == null) {
+                evt = new ConnectionEvent(this);
+            }
+            listener.connectionClosed(evt);
+        }
+    }
+
+    /**
+     * Used to fire a connection event to all listeners.
+     */
+    void fireConnectionFatalError(SQLException e) {
+        ConnectionEvent evt = null;
+        // Copy the listener list so the listener can remove itself during this method call
+        ConnectionEventListener[] local = (ConnectionEventListener[])listeners.toArray(new ConnectionEventListener[listeners.size()]);
+        for (int i=0; i<local.length; i++) {
+            ConnectionEventListener listener = local[i];
+            if (evt == null) {
+                evt = new ConnectionEvent(this, e);
+            }
+            listener.connectionErrorOccurred(evt);
+        }
+    }
+
+    /**
+     * Instead of declaring a class implementing Connection, which would have
+     * to be updated for every JDK rev, use a dynamic proxy to handle all
+     * calls through the Connection interface.  This is the part that
+     * requires JDK 1.3 or higher, though JDK 1.2 could be supported with a
+     * 3rd-party proxy package.
+     */
+    private class ConnectionHandler implements InvocationHandler {
+        private Connection con;
+        private boolean automatic = false;
+
+        public ConnectionHandler(Connection con) {
+            this.con = con;
+        }
+
+        public Object invoke(Object proxy, Method method, Object[] args)
+                throws Throwable {
+            // From Object
+            if(method.getDeclaringClass().getName().equals("java.lang.Object")) {
+                if(method.getName().equals("toString")) {
+                    return "Pooled connection wrapping physical connection "+con;
+                }
+                if(method.getName().equals("hashCode")) {
+                    return new Integer(con.hashCode());
+                }
+                if(method.getName().equals("equals")) {
+                    if(args[0] == null) {
+                        return Boolean.FALSE;
+                    }
+                    try {
+                        return Proxy.isProxyClass(args[0].getClass()) && ((ConnectionHandler) Proxy.getInvocationHandler(args[0])).con == con ? Boolean.TRUE : Boolean.FALSE;
+                    } catch(ClassCastException e) {
+                        return Boolean.FALSE;
+                    }
+                }
+                return method.invoke(con, args);
+            }
+            // All the rest is from the Connection interface
+            if(method.getName().equals("isClosed")) {
+                return con == null ? Boolean.TRUE : Boolean.FALSE;
+            }
+            if(con == null) {
+                throw new SQLException(automatic ? "Connection has been closed automatically because a new connection was opened for the same PooledConnection or the PooledConnection has been closed" : "Connection has been closed");
+            }
+            if(method.getName().equals("close")) {
+                SQLException ex = null;
+                if(!con.getAutoCommit()) {
+                    try {con.rollback();} catch(SQLException e) {ex = e;}
+                }
+                con.clearWarnings();
+                con = null;
+                last = null;
+                fireConnectionClosed();
+                if(ex != null) {
+                    throw ex;
+                }
+                return null;
+            } else {
+                return method.invoke(con, args);
+            }
+        }
+
+        public void close() {
+            if(con != null) {
+                automatic = true;
+            }
+            con = null;
+            // No close event fired here: see JDBC 2.0 Optional Package spec section 6.3
+        }
+    }
+}
diff --git a/src/interfaces/jdbc/org/postgresql/jdbc2/optional/PoolingDataSource.java b/src/interfaces/jdbc/org/postgresql/jdbc2/optional/PoolingDataSource.java
new file mode 100644 (file)
index 0000000..1ee2c5e
--- /dev/null
@@ -0,0 +1,413 @@
+package org.postgresql.jdbc2.optional;
+
+import javax.sql.*;
+import javax.naming.*;
+import java.util.*;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+/**
+ * DataSource which uses connection pooling.  <font color="red">Don't use this if
+ * your server/middleware vendor provides a connection pooling implementation
+ * which interfaces with the PostgreSQL ConnectionPoolDataSource implementation!</font>
+ * This class is provided as a convenience, but the JDBC Driver is really not
+ * supposed to handle the connection pooling algorithm.  Instead, the server or
+ * middleware product is supposed to handle the mechanics of connection pooling,
+ * and use the PostgreSQL implementation of ConnectionPoolDataSource to provide
+ * the connections to pool.
+ *
+ * <p>If you're sure you want to use this, then you must set the properties
+ * dataSourceName, databaseName, user, and password (if required for the user).
+ * The settings for serverName, portNumber, initialConnections, and
+ * maxConnections are optional.  Note that <i>only connections
+ * for the default user will be pooled!</i>  Connections for other users will
+ * be normal non-pooled connections, and will not count against the maximum pool
+ * size limit.</p>
+ *
+ * <p>If you put this DataSource in JNDI, and access it from different JVMs (or
+ * otherwise load this class from different ClassLoaders), you'll end up with one
+ * pool per ClassLoader or VM.  This is another area where a server-specific
+ * implementation may provide advanced features, such as using a single pool
+ * across all VMs in a cluster.</p>
+ *
+ * <p>This implementation supports JDK 1.3 and higher.</p>
+ *
+ * @author Aaron Mulder (ammulder@chariotsolutions.com)
+ * @version $Revision: 1.1 $
+ */
+public class PoolingDataSource extends BaseDataSource implements DataSource {
+    private static Map dataSources = new HashMap();
+
+    static PoolingDataSource getDataSource(String name) {
+        return (PoolingDataSource)dataSources.get(name);
+    }
+
+    // Additional Data Source properties
+    private String dataSourceName;
+    private int initialConnections = 0;
+    private int maxConnections = 0;
+    // State variables
+    private boolean initialized = false;
+    private Stack available = new Stack();
+    private Stack used = new Stack();
+    private Object lock = new Object();
+    private ConnectionPool source;
+
+    /**
+     * Gets a description of this DataSource.
+     */
+    public String getDescription() {
+        return "Pooling DataSource '"+dataSourceName+" from "+org.postgresql.Driver.getVersion();
+    }
+
+    /**
+     * Ensures the DataSource properties are not changed after the DataSource has
+     * been used.
+     *
+     * @throws java.lang.IllegalStateException
+     *         The Server Name cannot be changed after the DataSource has been
+     *         used.
+     */
+    public void setServerName(String serverName) {
+        if (initialized) {
+            throw new IllegalStateException("Cannot set Data Source properties after DataSource has been used");
+        }
+        super.setServerName(serverName);
+    }
+
+    /**
+     * Ensures the DataSource properties are not changed after the DataSource has
+     * been used.
+     *
+     * @throws java.lang.IllegalStateException
+     *         The Database Name cannot be changed after the DataSource has been
+     *         used.
+     */
+    public void setDatabaseName(String databaseName) {
+        if (initialized) {
+            throw new IllegalStateException("Cannot set Data Source properties after DataSource has been used");
+        }
+        super.setDatabaseName(databaseName);
+    }
+
+    /**
+     * Ensures the DataSource properties are not changed after the DataSource has
+     * been used.
+     *
+     * @throws java.lang.IllegalStateException
+     *         The User cannot be changed after the DataSource has been
+     *         used.
+     */
+    public void setUser(String user) {
+        if (initialized) {
+            throw new IllegalStateException("Cannot set Data Source properties after DataSource has been used");
+        }
+        super.setUser(user);
+    }
+
+    /**
+     * Ensures the DataSource properties are not changed after the DataSource has
+     * been used.
+     *
+     * @throws java.lang.IllegalStateException
+     *         The Password cannot be changed after the DataSource has been
+     *         used.
+     */
+    public void setPassword(String password) {
+        if (initialized) {
+            throw new IllegalStateException("Cannot set Data Source properties after DataSource has been used");
+        }
+        super.setPassword(password);
+    }
+
+    /**
+     * Ensures the DataSource properties are not changed after the DataSource has
+     * been used.
+     *
+     * @throws java.lang.IllegalStateException
+     *         The Port Number cannot be changed after the DataSource has been
+     *         used.
+     */
+    public void setPortNumber(int portNumber) {
+        if (initialized) {
+            throw new IllegalStateException("Cannot set Data Source properties after DataSource has been used");
+        }
+        super.setPortNumber(portNumber);
+    }
+
+    /**
+     * Gets the number of connections that will be created when this DataSource
+     * is initialized.  If you do not call initialize explicitly, it will be
+     * initialized the first time a connection is drawn from it.
+     */
+    public int getInitialConnections() {
+        return initialConnections;
+    }
+
+    /**
+     * Sets the number of connections that will be created when this DataSource
+     * is initialized.  If you do not call initialize explicitly, it will be
+     * initialized the first time a connection is drawn from it.
+     *
+     * @throws java.lang.IllegalStateException
+     *         The Initial Connections cannot be changed after the DataSource has been
+     *         used.
+     */
+    public void setInitialConnections(int initialConnections) {
+        if (initialized) {
+            throw new IllegalStateException("Cannot set Data Source properties after DataSource has been used");
+        }
+        this.initialConnections = initialConnections;
+    }
+
+    /**
+     * Gets the maximum number of connections that the pool will allow.  If a request
+     * comes in and this many connections are in use, the request will block until a
+     * connection is available.  Note that connections for a user other than the
+     * default user will not be pooled and don't count against this limit.
+     *
+     * @return The maximum number of pooled connection allowed, or 0 for no maximum.
+     */
+    public int getMaxConnections() {
+        return maxConnections;
+    }
+
+    /**
+     * Sets the maximum number of connections that the pool will allow.  If a request
+     * comes in and this many connections are in use, the request will block until a
+     * connection is available.  Note that connections for a user other than the
+     * default user will not be pooled and don't count against this limit.
+     *
+     * @param maxConnections The maximum number of pooled connection to allow, or
+     *        0 for no maximum.
+     *
+     * @throws java.lang.IllegalStateException
+     *         The Maximum Connections cannot be changed after the DataSource has been
+     *         used.
+     */
+    public void setMaxConnections(int maxConnections) {
+        if (initialized) {
+            throw new IllegalStateException("Cannot set Data Source properties after DataSource has been used");
+        }
+        this.maxConnections = maxConnections;
+    }
+
+    /**
+     * Gets the name of this DataSource.  This uniquely identifies the DataSource.
+     * You cannot use more than one DataSource in the same VM with the same name.
+     */
+    public String getDataSourceName() {
+        return dataSourceName;
+    }
+
+    /**
+     * Sets the name of this DataSource.  This is required, and uniquely identifies
+     * the DataSource.  You cannot create or use more than one DataSource in the
+     * same VM with the same name.
+     *
+     * @throws java.lang.IllegalStateException
+     *         The Data Source Name cannot be changed after the DataSource has been
+     *         used.
+     * @throws java.lang.IllegalArgumentException
+     *         Another PoolingDataSource with the same dataSourceName already
+     *         exists.
+     */
+    public void setDataSourceName(String dataSourceName) {
+        if(initialized) {
+            throw new IllegalStateException("Cannot set Data Source properties after DataSource has been used");
+        }
+        if(this.dataSourceName != null && dataSourceName != null && dataSourceName.equals(this.dataSourceName)) {
+            return;
+        }
+        synchronized(dataSources) {
+            if(getDataSource(dataSourceName) != null) {
+                throw new IllegalArgumentException("DataSource with name '"+dataSourceName+"' already exists!");
+            }
+            if (this.dataSourceName != null) {
+                dataSources.remove(this.dataSourceName);
+            }
+            this.dataSourceName = dataSourceName;
+            dataSources.put(dataSourceName, this);
+        }
+    }
+
+    /**
+     * Initializes this DataSource.  If the initialConnections is greater than zero,
+     * that number of connections will be created.  After this method is called,
+     * the DataSource properties cannot be changed.  If you do not call this
+     * explicitly, it will be called the first time you get a connection from the
+     * Datasource.
+     * @throws java.sql.SQLException
+     *         Occurs when the initialConnections is greater than zero, but the
+     *         DataSource is not able to create enough physical connections.
+     */
+    public void initialize() throws SQLException {
+        synchronized (lock) {
+            source = new ConnectionPool();
+            source.setDatabaseName(getDatabaseName());
+            source.setPassword(getPassword());
+            source.setPortNumber(getPortNumber());
+            source.setServerName(getServerName());
+            source.setUser(getUser());
+            while (available.size() < initialConnections) {
+                available.push(source.getPooledConnection());
+            }
+            initialized = true;
+        }
+    }
+
+    /**
+     * Gets a <b>non-pooled</b> connection, unless the user and password are the
+     * same as the default values for this connection pool.
+     *
+     * @return A pooled connection.
+     * @throws SQLException
+     *         Occurs when no pooled connection is available, and a new physical
+     *         connection cannot be created.
+     */
+    public Connection getConnection(String user, String password) throws SQLException {
+        // If this is for the default user/password, use a pooled connection
+        if(user == null ||
+           (user.equals(getUser()) && ((password == null && getPassword() == null) || (password != null && password.equals(getPassword()))))) {
+            return getConnection();
+        }
+        // Otherwise, use a non-pooled connection
+        if (!initialized) {
+            initialize();
+        }
+        return super.getConnection(user, password);
+    }
+
+    /**
+     * Gets a connection from the connection pool.
+     *
+     * @return A pooled connection.
+     * @throws SQLException
+     *         Occurs when no pooled connection is available, and a new physical
+     *         connection cannot be created.
+     */
+    public Connection getConnection() throws SQLException {
+        if(!initialized) {
+            initialize();
+        }
+        return getPooledConnection();
+    }
+
+    /**
+     * Closes this DataSource, and all the pooled connections, whether in use or not.
+     */
+    public void close() {
+        synchronized(lock) {
+            while(available.size() > 0) {
+                PooledConnectionImpl pci = (PooledConnectionImpl)available.pop();
+                try {
+                    pci.close();
+                } catch (SQLException e) {
+                }
+            }
+            available = null;
+            while (used.size() > 0) {
+                PooledConnectionImpl pci = (PooledConnectionImpl)used.pop();
+                pci.removeConnectionEventListener(connectionEventListener);
+                try {
+                    pci.close();
+                } catch (SQLException e) {
+                }
+            }
+            used = null;
+        }
+        synchronized (dataSources) {
+            dataSources.remove(dataSourceName);
+        }
+    }
+
+    /**
+     * Gets a connection from the pool.  Will get an available one if
+     * present, or create a new one if under the max limit.  Will
+     * block if all used and a new one would exceed the max.
+     */
+    private Connection getPooledConnection() throws SQLException {
+        PooledConnection pc = null;
+        synchronized(lock) {
+            if (available == null) {
+                throw new SQLException("DataSource has been closed.");
+            }
+            while(true) {
+                if(available.size() > 0) {
+                    pc = (PooledConnection)available.pop();
+                    used.push(pc);
+                    break;
+                }
+                if(maxConnections == 0 || used.size() < maxConnections) {
+                    pc = source.getPooledConnection();
+                    used.push(pc);
+                    break;
+                } else {
+                    try {
+                        // Wake up every second at a minimum
+                        lock.wait(1000L);
+                    } catch(InterruptedException e) {
+                    }
+                }
+            }
+        }
+        pc.addConnectionEventListener(connectionEventListener);
+        return pc.getConnection();
+    }
+
+    /**
+     * Notified when a pooled connection is closed, or a fatal error occurs
+     * on a pooled connection.  This is the only way connections are marked
+     * as unused.
+     */
+    private ConnectionEventListener connectionEventListener = new ConnectionEventListener() {
+        public void connectionClosed(ConnectionEvent event) {
+            ((PooledConnection)event.getSource()).removeConnectionEventListener(this);
+            synchronized(lock) {
+                if(available == null) {
+                    return; // DataSource has been closed
+                }
+                boolean removed = used.remove(event.getSource());
+                if(removed) {
+                    available.push(event.getSource());
+                    // There's now a new connection available
+                    lock.notify();
+                } else {
+                    // a connection error occured
+                }
+            }
+        }
+
+        /**
+         * This is only called for fatal errors, where the physical connection is
+         * useless afterward and should be removed from the pool.
+         */
+        public void connectionErrorOccurred(ConnectionEvent event) {
+            ((PooledConnection) event.getSource()).removeConnectionEventListener(this);
+            synchronized(lock) {
+                if (available == null) {
+                    return; // DataSource has been closed
+                }
+                used.remove(event.getSource());
+                // We're now at least 1 connection under the max
+                lock.notify();
+            }
+        }
+    };
+
+    /**
+     * Adds custom properties for this DataSource to the properties defined in
+     * the superclass.
+     */
+    public Reference getReference() throws NamingException {
+        Reference ref = super.getReference();
+        ref.add(new StringRefAddr("dataSourceName", dataSourceName));
+        if (initialConnections > 0) {
+            ref.add(new StringRefAddr("initialConnections", Integer.toString(initialConnections)));
+        }
+        if (maxConnections > 0) {
+            ref.add(new StringRefAddr("maxConnections", Integer.toString(maxConnections)));
+        }
+        return ref;
+    }
+}
diff --git a/src/interfaces/jdbc/org/postgresql/jdbc2/optional/SimpleDataSource.java b/src/interfaces/jdbc/org/postgresql/jdbc2/optional/SimpleDataSource.java
new file mode 100644 (file)
index 0000000..ca06efd
--- /dev/null
@@ -0,0 +1,22 @@
+package org.postgresql.jdbc2.optional;
+
+import javax.sql.DataSource;
+import java.io.Serializable;
+
+/**
+ * Simple DataSource which does not perform connection pooling.  In order to use
+ * the DataSource, you must set the property databaseName.  The settings for
+ * serverName, portNumber, user, and password are optional.  Note: these properties
+ * are declared in the superclass.
+ *
+ * @author Aaron Mulder (ammulder@chariotsolutions.com)
+ * @version $Revision: 1.1 $
+ */
+public class SimpleDataSource extends BaseDataSource implements Serializable, DataSource {
+    /**
+     * Gets a description of this DataSource.
+     */
+    public String getDescription() {
+        return "Non-Pooling DataSource from "+org.postgresql.Driver.getVersion();
+    }
+}
diff --git a/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/BaseDataSourceTest.java b/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/BaseDataSourceTest.java
new file mode 100644 (file)
index 0000000..f26fca2
--- /dev/null
@@ -0,0 +1,160 @@
+package org.postgresql.test.jdbc2.optional;
+
+import junit.framework.TestCase;
+import org.postgresql.test.JDBC2Tests;
+import org.postgresql.jdbc2.optional.SimpleDataSource;
+import org.postgresql.jdbc2.optional.BaseDataSource;
+
+import java.sql.*;
+
+/**
+ * Common tests for all the BaseDataSource implementations.  This is
+ * a small variety to make sure that a connection can be opened and
+ * some basic queries run.  The different BaseDataSource subclasses
+ * have different subclasses of this which add additional custom
+ * tests.
+ *
+ * @author Aaron Mulder (ammulder@chariotsolutions.com)
+ * @version $Revision: 1.1 $
+ */
+public abstract class BaseDataSourceTest extends TestCase {
+    protected Connection con;
+    protected BaseDataSource bds;
+
+    /**
+     * Constructor required by JUnit
+     */
+    public BaseDataSourceTest(String name) {
+        super(name);
+    }
+
+    /**
+     * Creates a test table using a standard connection (not from a
+     * DataSource).
+     */
+    protected void setUp() throws Exception {
+        con = JDBC2Tests.openDB();
+        JDBC2Tests.createTable(con, "poolingtest", "id int4 not null primary key, name varchar(50)");
+        Statement stmt = con.createStatement();
+        stmt.executeUpdate("INSERT INTO poolingtest VALUES (1, 'Test Row 1')");
+        stmt.executeUpdate("INSERT INTO poolingtest VALUES (2, 'Test Row 2')");
+        JDBC2Tests.closeDB(con);
+    }
+
+    /**
+     * Removes the test table using a standard connection (not from
+     * a DataSource)
+     */
+    protected void tearDown() throws Exception {
+        con = JDBC2Tests.openDB();
+        JDBC2Tests.dropTable(con, "poolingtest");
+        JDBC2Tests.closeDB(con);
+    }
+
+    /**
+     * Gets a connection from the current BaseDataSource
+     */
+    protected Connection getDataSourceConnection() throws SQLException {
+        initializeDataSource();
+        return bds.getConnection();
+    }
+
+    /**
+     * Creates an instance of the current BaseDataSource for
+     * testing.  Must be customized by each subclass.
+     */
+    protected abstract void initializeDataSource();
+
+    /**
+     * Test to make sure you can instantiate and configure the
+     * appropriate DataSource
+     */
+    public void testCreateDataSource() {
+        initializeDataSource();
+    }
+
+    /**
+     * Test to make sure you can get a connection from the DataSource,
+     * which in turn means the DataSource was able to open it.
+     */
+    public void testGetConnection() {
+        try {
+            con = getDataSourceConnection();
+            con.close();
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * A simple test to make sure you can execute SQL using the
+     * Connection from the DataSource
+     */
+    public void testUseConnection() {
+        try {
+            con = getDataSourceConnection();
+            Statement st = con.createStatement();
+            ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM poolingtest");
+            if(rs.next()) {
+                int count = rs.getInt(1);
+                if(rs.next()) {
+                    fail("Should only have one row in SELECT COUNT result set");
+                }
+                if(count != 2) {
+                    fail("Count returned "+count+" expecting 2");
+                }
+            } else {
+                fail("Should have one row in SELECT COUNT result set");
+            }
+            rs.close();
+            st.close();
+            con.close();
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * A test to make sure you can execute DDL SQL using the
+     * Connection from the DataSource.
+     */
+    public void testDdlOverConnection() {
+        try {
+            con = getDataSourceConnection();
+            JDBC2Tests.dropTable(con, "poolingtest");
+            JDBC2Tests.createTable(con, "poolingtest", "id int4 not null primary key, name varchar(50)");
+            con.close();
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * A test to make sure the connections are not being pooled by the
+     * current DataSource.  Obviously need to be overridden in the case
+     * of a pooling Datasource.
+     */
+    public void testNotPooledConnection() {
+        try {
+            con = getDataSourceConnection();
+            String name = con.toString();
+            con.close();
+            con = getDataSourceConnection();
+            String name2 = con.toString();
+            con.close();
+            assertTrue(!name.equals(name2));
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * Eventually, we must test stuffing the DataSource in JNDI and
+     * then getting it back out and make sure it's still usable.  This
+     * should ideally test both Serializable and Referenceable
+     * mechanisms.  Will probably be multiple tests when implemented.
+     */
+    public void testJndi() {
+        // TODO: Put the DS in JNDI, retrieve it, and try some of this stuff again
+    }
+}
diff --git a/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/ConnectionPoolTest.java b/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/ConnectionPoolTest.java
new file mode 100644 (file)
index 0000000..5802f15
--- /dev/null
@@ -0,0 +1,327 @@
+package org.postgresql.test.jdbc2.optional;
+
+import org.postgresql.jdbc2.optional.ConnectionPool;
+import org.postgresql.test.JDBC2Tests;
+import javax.sql.*;
+import java.sql.*;
+
+/**
+ * Tests for the ConnectionPoolDataSource and PooledConnection
+ * implementations.  They are tested together because the only client
+ * interface to the PooledConnection is through the CPDS.
+ *
+ * @author Aaron Mulder (ammulder@chariotsolutions.com)
+ * @version $Revision: 1.1 $
+ */
+public class ConnectionPoolTest extends BaseDataSourceTest {
+    /**
+     * Constructor required by JUnit
+     */
+    public ConnectionPoolTest(String name) {
+        super(name);
+    }
+
+    /**
+     * Creates and configures a ConnectionPool
+     */
+    protected void initializeDataSource() {
+        if(bds == null) {
+            bds = new ConnectionPool();
+            String db = JDBC2Tests.getURL();
+            if(db.indexOf('/') > -1) {
+                db = db.substring(db.lastIndexOf('/')+1);
+            } else if(db.indexOf(':') > -1) {
+                db = db.substring(db.lastIndexOf(':')+1);
+            }
+            bds.setDatabaseName(db);
+            bds.setUser(JDBC2Tests.getUser());
+            bds.setPassword(JDBC2Tests.getPassword());
+        }
+    }
+
+    /**
+     * Though the normal client interface is to grab a Connection, in
+     * order to test the middleware/server interface, we need to deal
+     * with PooledConnections.  Some tests use each.
+     */
+    protected PooledConnection getPooledConnection() throws SQLException {
+        initializeDataSource();
+        return ((ConnectionPool)bds).getPooledConnection();
+    }
+
+    /**
+     * Instead of just fetching a Connection from the ConnectionPool,
+     * get a PooledConnection, add a listener to close it when the
+     * Connection is closed, and then get the Connection.  Without
+     * the listener the PooledConnection (and thus the physical connection)
+     * would never by closed.  Probably not a disaster during testing, but
+     * you never know.
+     */
+    protected Connection getDataSourceConnection() throws SQLException {
+        initializeDataSource();
+        final PooledConnection pc = getPooledConnection();
+        // Since the pooled connection won't be reused in these basic tests, close it when the connection is closed
+        pc.addConnectionEventListener(new ConnectionEventListener() {
+            public void connectionClosed(ConnectionEvent event) {
+                try {
+                    pc.close();
+                } catch (SQLException e) {
+                    fail("Unable to close PooledConnection: "+e);
+                }
+            }
+
+            public void connectionErrorOccurred(ConnectionEvent event) {
+            }
+        });
+        return pc.getConnection();
+    }
+
+    /**
+     * Makes sure that if you get a connection from a PooledConnection,
+     * close it, and then get another one, you're really using the same
+     * physical connection.  Depends on the implementation of toString
+     * for the connection handle.
+     */
+    public void testPoolReuse() {
+        try {
+            PooledConnection pc = getPooledConnection();
+            con = pc.getConnection();
+            String name = con.toString();
+            con.close();
+            con = pc.getConnection();
+            String name2 = con.toString();
+            con.close();
+            pc.close();
+            assertTrue("Physical connection doesn't appear to be reused across PooledConnection wrappers", name.equals(name2));
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * Makes sure that when you request a connection from the
+     * PooledConnection, and previous connection it might have given
+     * out is closed.  See JDBC 2.0 Optional Package spec section
+     * 6.2.3
+     */
+    public void testPoolCloseOldWrapper() {
+        try {
+            PooledConnection pc = getPooledConnection();
+            con = pc.getConnection();
+            Connection con2 = pc.getConnection();
+            try {
+                con.createStatement();
+                fail("Original connection wrapper should be closed when new connection wrapper is generated");
+            } catch(SQLException e) {}
+            try {
+                con.close();
+                fail("Original connection wrapper should be closed when new connection wrapper is generated");
+            } catch(SQLException e) {}
+            con2.close();
+            pc.close();
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * Makes sure that if you get two connection wrappers from the same
+     * PooledConnection, they are different, even though the represent
+     * the same physical connection.  See JDBC 2.0 Optional Pacakge spec
+     * section 6.2.2
+     */
+    public void testPoolNewWrapper() {
+        try {
+            PooledConnection pc = getPooledConnection();
+            con = pc.getConnection();
+            Connection con2 = pc.getConnection();
+            con2.close();
+            pc.close();
+            assertTrue("Two calls to PooledConnection.getConnection should not return the same connection wrapper", con != con2);
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * Makes sure that exactly one close event is fired for each time a
+     * connection handle is closed.  Also checks that events are not
+     * fired after a given handle has been closed once.
+     */
+    public void testCloseEvent() {
+        try {
+            PooledConnection pc = getPooledConnection();
+            CountClose cc = new CountClose();
+            pc.addConnectionEventListener(cc);
+            con = pc.getConnection();
+            assertTrue(cc.getCount() == 0);
+            assertTrue(cc.getErrorCount() == 0);
+            con.close();
+            assertTrue(cc.getCount() == 1);
+            assertTrue(cc.getErrorCount() == 0);
+            con = pc.getConnection();
+            assertTrue(cc.getCount() == 1);
+            assertTrue(cc.getErrorCount() == 0);
+            con.close();
+            assertTrue(cc.getCount() == 2);
+            assertTrue(cc.getErrorCount() == 0);
+            try {
+                con.close();
+                fail("Should not be able to close a connection wrapper twice");
+            } catch (SQLException e) {}
+            assertTrue(cc.getCount() == 2);
+            assertTrue(cc.getErrorCount() == 0);
+            pc.close();
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * Makes sure that close events are not fired after a listener has
+     * been removed.
+     */
+    public void testNoCloseEvent() {
+        try {
+            PooledConnection pc = getPooledConnection();
+            CountClose cc = new CountClose();
+            pc.addConnectionEventListener(cc);
+            con = pc.getConnection();
+            assertTrue(cc.getCount() == 0);
+            assertTrue(cc.getErrorCount() == 0);
+            con.close();
+            assertTrue(cc.getCount() == 1);
+            assertTrue(cc.getErrorCount() == 0);
+            pc.removeConnectionEventListener(cc);
+            con = pc.getConnection();
+            assertTrue(cc.getCount() == 1);
+            assertTrue(cc.getErrorCount() == 0);
+            con.close();
+            assertTrue(cc.getCount() == 1);
+            assertTrue(cc.getErrorCount() == 0);
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * Makes sure that a listener can be removed while dispatching
+     * events.  Sometimes this causes a ConcurrentModificationException
+     * or something.
+     */
+    public void testInlineCloseEvent() {
+        try {
+            PooledConnection pc = getPooledConnection();
+            RemoveClose rc1 = new RemoveClose();
+            RemoveClose rc2 = new RemoveClose();
+            RemoveClose rc3 = new RemoveClose();
+            pc.addConnectionEventListener(rc1);
+            pc.addConnectionEventListener(rc2);
+            pc.addConnectionEventListener(rc3);
+            con = pc.getConnection();
+            con.close();
+            con = pc.getConnection();
+            con.close();
+        } catch (Exception e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * Tests that a close event is not generated when a connection
+     * handle is closed automatically due to a new connection handle
+     * being opened for the same PooledConnection.  See JDBC 2.0
+     * Optional Package spec section 6.3
+     */
+    public void testAutomaticCloseEvent() {
+        try {
+            PooledConnection pc = getPooledConnection();
+            CountClose cc = new CountClose();
+            pc.addConnectionEventListener(cc);
+            con = pc.getConnection();
+            assertTrue(cc.getCount() == 0);
+            assertTrue(cc.getErrorCount() == 0);
+            con.close();
+            assertTrue(cc.getCount() == 1);
+            assertTrue(cc.getErrorCount() == 0);
+            con = pc.getConnection();
+            assertTrue(cc.getCount() == 1);
+            assertTrue(cc.getErrorCount() == 0);
+            // Open a 2nd connection, causing the first to be closed.  No even should be generated.
+            Connection con2 = pc.getConnection();
+            assertTrue("Connection handle was not closed when new handle was opened", con.isClosed());
+            assertTrue(cc.getCount() == 1);
+            assertTrue(cc.getErrorCount() == 0);
+            con2.close();
+            assertTrue(cc.getCount() == 2);
+            assertTrue(cc.getErrorCount() == 0);
+            pc.close();
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * Makes sure the isClosed method on a connection wrapper does what
+     * you'd expect.  Checks the usual case, as well as automatic
+     * closure when a new handle is opened on the same physical connection.
+     */
+    public void testIsClosed() {
+        try {
+            PooledConnection pc = getPooledConnection();
+            Connection con = pc.getConnection();
+            assertTrue(!con.isClosed());
+            con.close();
+            assertTrue(con.isClosed());
+            con = pc.getConnection();
+            Connection con2 = pc.getConnection();
+            assertTrue(con.isClosed());
+            assertTrue(!con2.isClosed());
+            con2.close();
+            assertTrue(con.isClosed());
+            pc.close();
+        } catch (SQLException e) {
+            fail(e.getMessage());
+        }
+    }
+
+    /**
+     * Helper class to remove a listener during event dispatching.
+     */
+    private class RemoveClose implements ConnectionEventListener {
+        public void connectionClosed(ConnectionEvent event) {
+            ((PooledConnection)event.getSource()).removeConnectionEventListener(this);
+        }
+
+        public void connectionErrorOccurred(ConnectionEvent event) {
+            ((PooledConnection)event.getSource()).removeConnectionEventListener(this);
+        }
+    }
+
+    /**
+     * Helper class that implements the event listener interface, and
+     * counts the number of events it sees.
+     */
+    private class CountClose implements ConnectionEventListener {
+        private int count = 0, errorCount = 0;
+        public void connectionClosed(ConnectionEvent event) {
+            count++;
+        }
+
+        public void connectionErrorOccurred(ConnectionEvent event) {
+            errorCount++;
+        }
+
+        public int getCount() {
+            return count;
+        }
+
+        public int getErrorCount() {
+            return errorCount;
+        }
+
+        public void clear() {
+            count = errorCount = 0;
+        }
+    }
+}
diff --git a/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/OptionalTestSuite.java b/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/OptionalTestSuite.java
new file mode 100644 (file)
index 0000000..ded7503
--- /dev/null
@@ -0,0 +1,24 @@
+package org.postgresql.test.jdbc2.optional;
+
+import junit.framework.TestSuite;
+
+/**
+ * Test suite for the JDBC 2.0 Optional Package implementation.  This
+ * includes the DataSource, ConnectionPoolDataSource, and
+ * PooledConnection implementations.
+ *
+ * @author Aaron Mulder (ammulder@chariotsolutions.com)
+ * @version $Revision: 1.1 $
+ */
+public class OptionalTestSuite extends TestSuite {
+    /**
+     * Gets the test suite for the entire JDBC 2.0 Optional Package
+     * implementation.
+     */
+    public static TestSuite suite() {
+        TestSuite suite = new TestSuite();
+        suite.addTestSuite(SimpleDataSourceTest.class);
+        suite.addTestSuite(ConnectionPoolTest.class);
+        return suite;
+    }
+}
diff --git a/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/SimpleDataSourceTest.java b/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/SimpleDataSourceTest.java
new file mode 100644 (file)
index 0000000..1c917db
--- /dev/null
@@ -0,0 +1,38 @@
+package org.postgresql.test.jdbc2.optional;
+
+import org.postgresql.test.JDBC2Tests;
+import org.postgresql.jdbc2.optional.SimpleDataSource;
+
+/**
+ * Performs the basic tests defined in the superclass.  Just adds the
+ * configuration logic.
+ *
+ * @author Aaron Mulder (ammulder@chariotsolutions.com)
+ * @version $Revision: 1.1 $
+ */
+public class SimpleDataSourceTest extends BaseDataSourceTest {
+    /**
+     * Constructor required by JUnit
+     */
+    public SimpleDataSourceTest(String name) {
+        super(name);
+    }
+
+    /**
+     * Creates and configures a new SimpleDataSource.
+     */
+    protected void initializeDataSource() {
+        if(bds == null) {
+            bds = new SimpleDataSource();
+            String db = JDBC2Tests.getURL();
+            if(db.indexOf('/') > -1) {
+                db = db.substring(db.lastIndexOf('/')+1);
+            } else if(db.indexOf(':') > -1) {
+                db = db.substring(db.lastIndexOf(':')+1);
+            }
+            bds.setDatabaseName(db);
+            bds.setUser(JDBC2Tests.getUser());
+            bds.setPassword(JDBC2Tests.getPassword());
+        }
+    }
+}