From c82fed3d87e6a80044c97096340f6475a2ae5588 Mon Sep 17 00:00:00 2001 From: Dave Cramer Date: Tue, 30 Jul 2002 11:41:10 +0000 Subject: [PATCH] Added DataSource code and tests submitted by Aaron Mulder --- .../jdbc2/optional/BaseDataSource.java | 224 ++++++++++ .../jdbc2/optional/ConnectionPool.java | 76 ++++ .../jdbc2/optional/PGObjectFactory.java | 89 ++++ .../jdbc2/optional/PooledConnectionImpl.java | 199 +++++++++ .../jdbc2/optional/PoolingDataSource.java | 413 ++++++++++++++++++ .../jdbc2/optional/SimpleDataSource.java | 22 + .../jdbc2/optional/BaseDataSourceTest.java | 160 +++++++ .../jdbc2/optional/ConnectionPoolTest.java | 327 ++++++++++++++ .../jdbc2/optional/OptionalTestSuite.java | 24 + .../jdbc2/optional/SimpleDataSourceTest.java | 38 ++ 10 files changed, 1572 insertions(+) create mode 100644 src/interfaces/jdbc/org/postgresql/jdbc2/optional/BaseDataSource.java create mode 100644 src/interfaces/jdbc/org/postgresql/jdbc2/optional/ConnectionPool.java create mode 100644 src/interfaces/jdbc/org/postgresql/jdbc2/optional/PGObjectFactory.java create mode 100644 src/interfaces/jdbc/org/postgresql/jdbc2/optional/PooledConnectionImpl.java create mode 100644 src/interfaces/jdbc/org/postgresql/jdbc2/optional/PoolingDataSource.java create mode 100644 src/interfaces/jdbc/org/postgresql/jdbc2/optional/SimpleDataSource.java create mode 100644 src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/BaseDataSourceTest.java create mode 100644 src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/ConnectionPoolTest.java create mode 100644 src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/OptionalTestSuite.java create mode 100644 src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/SimpleDataSourceTest.java 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 index 0000000000..0e3ae8e766 --- /dev/null +++ b/src/interfaces/jdbc/org/postgresql/jdbc2/optional/BaseDataSource.java @@ -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 localhost. + */ + 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 index 0000000000..1ffee5d342 --- /dev/null +++ b/src/interfaces/jdbc/org/postgresql/jdbc2/optional/ConnectionPool.java @@ -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. + * + *

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.

+ * + *

This implementation supports JDK 1.3 and higher.

+ * + * @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 false, 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 false, 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 index 0000000000..0663062b74 --- /dev/null +++ b/src/interfaces/jdbc/org/postgresql/jdbc2/optional/PGObjectFactory.java @@ -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 index 0000000000..bbd801e471 --- /dev/null +++ b/src/interfaces/jdbc/org/postgresql/jdbc2/optional/PooledConnectionImpl.java @@ -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. + * + *

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.

+ */ + 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; iDon't use this if + * your server/middleware vendor provides a connection pooling implementation + * which interfaces with the PostgreSQL ConnectionPoolDataSource implementation! + * 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. + * + *

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 only connections + * for the default user will be pooled! Connections for other users will + * be normal non-pooled connections, and will not count against the maximum pool + * size limit.

+ * + *

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.

+ * + *

This implementation supports JDK 1.3 and higher.

+ * + * @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 non-pooled 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 index 0000000000..ca06efd355 --- /dev/null +++ b/src/interfaces/jdbc/org/postgresql/jdbc2/optional/SimpleDataSource.java @@ -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 index 0000000000..f26fca20c9 --- /dev/null +++ b/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/BaseDataSourceTest.java @@ -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 index 0000000000..5802f156ca --- /dev/null +++ b/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/ConnectionPoolTest.java @@ -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 index 0000000000..ded7503a45 --- /dev/null +++ b/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/OptionalTestSuite.java @@ -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 index 0000000000..1c917db6a1 --- /dev/null +++ b/src/interfaces/jdbc/org/postgresql/test/jdbc2/optional/SimpleDataSourceTest.java @@ -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()); + } + } +} -- 2.40.0