Commit c82fed3d authored by Dave Cramer's avatar Dave Cramer

Added DataSource code and tests submitted by Aaron Mulder

parent 6410c222
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;
}
}
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;
}
}
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();
}
}
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
}
}
}
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();
}
}
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
}
}
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;
}
}
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());
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment