/*
$Header: /cvsroot/xorm/xorm/src/org/xorm/datastore/sql/PooledDataSource.java,v 1.7 2003/07/12 00:57:29 sbendar Exp $
This file is part of XORM.
XORM is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
XORM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with XORM; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.xorm.datastore.sql;
import java.util.Iterator;
import java.util.Properties;
import java.util.LinkedList;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.SQLException;
import java.sql.Driver;
import javax.sql.DataSource;
/**
* Wraps a JDBC driver manager and provides a pooling of connections.
*
* @author Scott Bendar
* @version $Revision: 1.7 $
*/
public class PooledDataSource implements DataSource {
private static final Logger logger =
Logger.getLogger("org.xorm.datastore.sql");
/**
* Milliseconds to wait for a connection to become available
* before waking up and logging the wait before waiting again.
* This is overridden if the loginTimeout is less than this.
*/
private static final int DEFAULT_TIMEOUT_MS = 5000;
private String connectionURL = null;
private String driverName = null;
private int minPool = 0;
private int maxPool = 5;
private int loginTimeout = 0;
private boolean checkReturnedConnection = false;
private long idleCheck = 0;
private Integer isolationLevel;
private String idleCheckSQL = null;
protected Properties props = new Properties();
protected int connCount = 0;
protected Driver driver = null;
protected LinkedList availableConnections = new LinkedList();
/**
* Initializes a new pooled data source
*/
public PooledDataSource() {
}
/**
* Sets the user name to pass when making a db connection
*/
public void setUser(String s) {
props.setProperty("user", s);
}
public String getUser() {
return props.getProperty("user");
}
/**
* Sets the password to use when creating a db connection. For
* security reasons, there is no way to get the password.
*/
public void setPassword(String s) {
props.setProperty("password", s);
}
/**
* Set the URL used to connect to the DB
*/
public void setConnectionUrl(String s) {
connectionURL = s;
}
public String getConnectionUrl() {
return connectionURL;
}
/**
* Set the name of the driver to load when connecting to the DB
*/
public void setDriverName(String s) {
driverName = s;
}
public String getDriverName() {
return driverName;
}
/**
* The maximum number of connections created by the data source.
* Once the max is exceeded callers must wait for a connection to
* become available.
*/
public int getMaxPool() {
return maxPool;
}
public void setMaxPool(int i) {
if (i < 1) {
logger.info("Max pool size is disabled, setting to max int.");
i = Integer.MAX_VALUE;
}
maxPool = i;
}
/**
* The minimum number of connections in the pool. It will create
* this number of connections when the pool is first initialized
* and the pool will never shrink below this many connections.
*/
public int getMinPool() {
return minPool;
}
public void setMinPool(int i) {
minPool = i;
}
public int getLoginTimeout() {
return loginTimeout;
}
/**
* Sets the maximum time to wait in seconds for a connection
* before throwing an exception.
*/
public void setLoginTimeout(int timeout) {
loginTimeout = timeout;
}
/**
* If set, instructs the connection to verify its valid when the
* caller is done with the connection (calls close). This is used
* for debugging purposes only to help catch folks to return closed
* connections to the pool.
*/
public boolean getCheckReturnedConnection() {
return checkReturnedConnection;
}
/**
* If set, instructs the connection to verify its valid when the
* caller is done with the connection (calls close). This is used
* for debugging purposes only to help catch folks to return closed
* connections to the pool.
*/
public void setCheckReturnedConnection(boolean value) {
checkReturnedConnection = value;
}
/**
* Gets the amount of time in milliseconds a connection has been
* idle before the pool tests the connection to see if its still
* valid.
*
* -1: Never check
* 0: Always check
*
*/
public long getIdleCheck() {
return idleCheck;
}
/**
* Sets the amount of time in milliseconds a connection has been
* idle before the pool tests the connection to see if its still
* valid.
*
* -1: Never check
* 0: Always check
*
*/
public void setIdleCheck(long idle) {
idleCheck = idle;
}
/**
* Gets a sql statement to be used to check a connection. If not
* specified, isConnectionClosed() is called instead.
*/
public String getIdleCheckSQL() {
return idleCheckSQL;
}
/**
* Sets a sql statement to be used to check a connection. If not
* specified, isConnectionClosed() is called instead.
*/
public void setIdleCheckSQL(String sql) {
idleCheckSQL = sql;
}
/**
* Sets the transaction isolation level. If null, no specific
* setting is made on a Connection instance; otherwise, the wrapped
* value is used.
*/
public void setIsolationLevel(Integer isolationLevel) {
this.isolationLevel = isolationLevel;
}
public Integer getIsolationLevel() {
return isolationLevel;
}
/**
* Gets a DB connection, actually a wrapper that allows the
* connection to be pooled. The method first allocates a
* connection from the available pool, if there are none available
* it creates a new connection unless that would put it over the
* max pool size, in which case it forces the caller to wait.
*
* If the data source was configured with a loginTimeout value
* greater than 0, it will wait for loginTimeout seconds for a
* connection.
*/
public Connection getConnection() throws SQLException {
PooledConnection conn = null;
int timeSpent = 0;
// Don't wait forever, wake up and check things out periodically
int waitTime = DEFAULT_TIMEOUT_MS;
// If there is a fixed time we are willing to wait, just wait that
// long
if (loginTimeout > 0)
waitTime = loginTimeout;
synchronized(availableConnections) {
// Initialize the driver the first time its used
if (driver == null)
initDriver();
while (loginTimeout == 0 || timeSpent < loginTimeout) {
long startTime = System.currentTimeMillis();
if (availableConnections.size() > 0) {
logger.fine("getConnection(): using available connection");
conn = (PooledConnection)availableConnections.removeFirst();
conn.setClosed(false);
try {
testConnection(conn);
return conn;
}
catch (SQLException e) {
/*
* If it fails the test, get rid of the connection
* so we will try and allocate a new one before
* returning a SQLException to the caller.
*/
logger.log(Level.SEVERE, "Connection failed test, dropping connection from pool!", e);
dropConnection(conn);
}
}
else {
if (connCount < maxPool) {
logger.fine("getConnection(): creating new connection");
conn = createNewConnection();
return conn;
}
else {
logger.fine("getConnection(): waiting for available connection");
try {
availableConnections.wait(waitTime);
}
catch (InterruptedException e) {
logger.fine("getConnection(): wait interrupted!");
}
timeSpent += (int)(System.currentTimeMillis() -
startTime);
logger.fine("getConnection(): wait timeout, wait total is " + timeSpent + " seconds");
}
}
}
}
logger.warning("getConnection(): wait exceeded loginTimeout");
throw new SQLException("getConnection(): wait exceeded loginTimeout");
}
/**
* This function is currently not implemented as it doesn't allow
* us to use the connections in the pool. The workaround is to
* create a new data source with a different user and password.
*
* Should this just pass back an unpooled real connection?
*/
public Connection getConnection(String userName, String password)
throws SQLException {
throw new SQLException("getConnection(user,pass) not supported, create a new data source with desired user and password");
}
/**
* The PooledDataSource class uses JDK 1.4 logging and ignores the
* log writer.
*/
public PrintWriter getLogWriter() {
return null;
}
public void setLogWriter(PrintWriter logger) {
}
/**
* Initialize the driver and create the initial connections up to
* the min pool size.
*/
private void initDriver() throws SQLException {
if (driver != null)
return;
logger.finer("initDriver() -- Entry");
try {
Class driverClass = Class.forName(driverName);
driver = (Driver)driverClass.newInstance();
// Create the minimum connections
while (connCount < minPool) {
availableConnections.add(createNewConnection());
}
}
catch (Throwable t) {
throw new SQLException("initDriver: " + t);
}
}
/**
* Creats a new physical db connection and wraps it in our
* PooledConnection
*/
private PooledConnection createNewConnection() throws SQLException {
PooledConnection conn = null;
if (driver != null) {
conn = new PooledConnection(this,
driver.connect(connectionURL, props));
if (isolationLevel != null) {
conn.setTransactionIsolation(isolationLevel.intValue());
}
connCount++;
}
return conn;
}
/**
* Tests if the connection is still valid or not.
*/
private void testConnection(PooledConnection conn) throws SQLException {
if (conn.hasReceivedException() ||
(idleCheck >= 0 &&
(System.currentTimeMillis() - conn.getLastUsedTime() > idleCheck))) {
conn.clearReceivedException();
if (!isConnectionValid(conn))
throw new SQLException("Connection is invalid, remove from pool.");
}
}
/**
* Checks if a connection is active by running a SQL query specified
* by IdleCheckSQL or by calling isClosed() if no sql statement has
* been specified.
*/
private boolean isConnectionValid(PooledConnection conn)
throws SQLException {
if (idleCheckSQL == null) {
logger.fine("Checking connection using isClosed");
return !conn.isConnectionClosed();
}
else {
logger.fine("Checking connection with SQL: " + idleCheckSQL);
Statement statement = conn.createStatement();
try {
statement.execute(idleCheckSQL);
}
finally {
statement.close();
}
return true;
}
}
/**
* Makes the connection available again. This is called by the
* PooledConnection class when the user chooses to close a
* connection.
*/
protected void makeAvailable(PooledConnection conn) {
synchronized (availableConnections) {
availableConnections.add(conn);
logger.fine("makeAvailable(): available=" +
availableConnections.size() + " total=" +
connCount);
availableConnections.notify();
}
}
/** Releases all available connections. */
// TODO: handle closure of connections that are currently leased
public void close() {
synchronized (availableConnections) {
Iterator i = availableConnections.iterator();
while (i.hasNext()) {
PooledConnection conn = (PooledConnection) i.next();
conn.closeConnection();
i.remove();
connCount--;
}
}
}
/** remove a connection from the pool */
protected void dropConnection(PooledConnection conn) {
synchronized (availableConnections) {
conn.closeConnection();
connCount--;
}
}
}
|