diff --git a/.hgignore b/.hgignore index 9d412d9..f39b5df 100644 --- a/.hgignore +++ b/.hgignore @@ -5,6 +5,7 @@ nbproject/** dist/** nbproject/** build.xml +jarlib/** \.orig$ \.orig\..*$ \.chg\..*$ diff --git a/src/uk/org/whoami/authme/datasource/MiniConnectionPoolManager.java b/src/uk/org/whoami/authme/datasource/MiniConnectionPoolManager.java new file mode 100644 index 0000000..61c0e89 --- /dev/null +++ b/src/uk/org/whoami/authme/datasource/MiniConnectionPoolManager.java @@ -0,0 +1,314 @@ +// Copyright 2007-2011 Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland +// www.source-code.biz, www.inventec.ch/chdh +// +// This module is multi-licensed and may be used under the terms +// of any of the following licenses: +// +// EPL, Eclipse Public License, http://www.eclipse.org/legal +// LGPL, GNU Lesser General Public License, http://www.gnu.org/licenses/lgpl.html +// MPL, Mozilla Public License 1.1, http://www.mozilla.org/MPL +// +// Please contact the author if you need another license. +// This module is provided "as is", without warranties of any kind. + +package uk.org.whoami.authme.datasource; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.LinkedList; +import javax.sql.ConnectionEvent; +import javax.sql.ConnectionEventListener; +import javax.sql.ConnectionPoolDataSource; +import javax.sql.PooledConnection; + +/** +* A lightweight standalone JDBC connection pool manager. +* +*
The public methods of this class are thread-safe. +* +*
Home page: www.source-code.biz/miniconnectionpoolmanager If If a connection is not valid, the method tries to get another connection
+* until one is valid (or a timeout occurs).
+*
+* Pooled connections may become invalid when e.g. the database server is
+* restarted.
+*
+* This method is slower than {@link #getConnection()} because the JDBC
+* driver has to send an extra command to the database server to test the connection.
+*
+*
+* This method requires Java 1.6 or newer.
+*
+* @throws TimeoutException
+* when no valid connection becomes available within This is the number of This is the number of internally kept recycled connections,
+* for which
+* Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland
+* Multi-licensed: EPL / LGPL / MPL.
+*/
+public class MiniConnectionPoolManager {
+
+private ConnectionPoolDataSource dataSource;
+private int maxConnections;
+private long timeoutMs;
+private PrintWriter logWriter;
+private Semaphore semaphore;
+private LinkedListtimeout
seconds.
+*/
+public static class TimeoutException extends RuntimeException {
+ private static final long serialVersionUID = 1;
+ public TimeoutException () {
+ super("Timeout while waiting for a free database connection."); }
+ public TimeoutException (String msg) {
+ super(msg); }}
+
+/**
+* Constructs a MiniConnectionPoolManager object with a timeout of 60 seconds.
+*
+* @param dataSource
+* the data source for the connections.
+* @param maxConnections
+* the maximum number of connections.
+*/
+public MiniConnectionPoolManager (ConnectionPoolDataSource dataSource, int maxConnections) {
+ this(dataSource, maxConnections, 60); }
+
+/**
+* Constructs a MiniConnectionPoolManager object.
+*
+* @param dataSource
+* the data source for the connections.
+* @param maxConnections
+* the maximum number of connections.
+* @param timeout
+* the maximum time in seconds to wait for a free connection.
+*/
+public MiniConnectionPoolManager (ConnectionPoolDataSource dataSource, int maxConnections, int timeout) {
+ this.dataSource = dataSource;
+ this.maxConnections = maxConnections;
+ this.timeoutMs = timeout * 1000L;
+ try {
+ logWriter = dataSource.getLogWriter(); }
+ catch (SQLException e) {}
+ if (maxConnections < 1) {
+ throw new IllegalArgumentException("Invalid maxConnections value."); }
+ semaphore = new Semaphore(maxConnections,true);
+ recycledConnections = new LinkedListmaxConnections
connections are already in use, the method
+* waits until a connection becomes available or timeout
seconds elapsed.
+* When the application is finished using the connection, it must close it
+* in order to return it to the pool.
+*
+* @return
+* a new Connection
object.
+* @throws TimeoutException
+* when no connection becomes available within timeout
seconds.
+*/
+public Connection getConnection() throws SQLException {
+ return getConnection2(timeoutMs); }
+
+private Connection getConnection2 (long timeoutMs) throws SQLException {
+ // This routine is unsynchronized, because semaphore.tryAcquire() may block.
+ synchronized (this) {
+ if (isDisposed) {
+ throw new IllegalStateException("Connection pool has been disposed."); }}
+ try {
+ if (!semaphore.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS)) {
+ throw new TimeoutException(); }}
+ catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while waiting for a database connection.",e); }
+ boolean ok = false;
+ try {
+ Connection conn = getConnection3();
+ ok = true;
+ return conn; }
+ finally {
+ if (!ok) {
+ semaphore.release(); }}}
+
+private synchronized Connection getConnection3() throws SQLException {
+ if (isDisposed) {
+ throw new IllegalStateException("Connection pool has been disposed."); } // test again with lock
+ PooledConnection pconn;
+ if (!recycledConnections.isEmpty()) {
+ pconn = recycledConnections.remove(); }
+ else {
+ pconn = dataSource.getPooledConnection();
+ pconn.addConnectionEventListener(poolConnectionEventListener); }
+ Connection conn = pconn.getConnection();
+ activeConnections++;
+ assertInnerState();
+ return conn; }
+
+/**
+* Retrieves a connection from the connection pool and ensures that it is valid
+* by calling {@link Connection#isValid(int)}.
+*
+* timeout
seconds.
+*/
+public Connection getValidConnection() {
+ long time = System.currentTimeMillis();
+ long timeoutTime = time + timeoutMs;
+ int triesWithoutDelay = getInactiveConnections() + 1;
+ while (true) {
+ Connection conn = getValidConnection2(time, timeoutTime);
+ if (conn != null) {
+ return conn; }
+ triesWithoutDelay--;
+ if (triesWithoutDelay <= 0) {
+ triesWithoutDelay = 0;
+ try {
+ Thread.sleep(250); }
+ catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while waiting for a valid database connection.", e); }}
+ time = System.currentTimeMillis();
+ if (time >= timeoutTime) {
+ throw new TimeoutException("Timeout while waiting for a valid database connection."); }}}
+
+private Connection getValidConnection2 (long time, long timeoutTime) {
+ long rtime = Math.max(1, timeoutTime - time);
+ Connection conn;
+ try {
+ conn = getConnection2(rtime); }
+ catch (SQLException e) {
+ return null; }
+ rtime = timeoutTime - System.currentTimeMillis();
+ int rtimeSecs = Math.max(1, (int)((rtime+999)/1000));
+ try {
+ if (conn.isValid(rtimeSecs)) {
+ return conn; }}
+ catch (SQLException e) {}
+ // This Exception should never occur. If it nevertheless occurs, it's because of an error in the
+ // JDBC driver which we ignore and assume that the connection is not valid.
+ // When isValid() returns false, the JDBC driver should have already called connectionErrorOccurred()
+ // and the PooledConnection has been removed from the pool, i.e. the PooledConnection will
+ // not be added to recycledConnections when Connection.close() is called.
+ // But to be sure that this works even with a faulty JDBC driver, we call purgeConnection().
+ purgeConnection(conn);
+ return null; }
+
+// Purges the PooledConnection associated with the passed Connection from the connection pool.
+private synchronized void purgeConnection (Connection conn) {
+ try {
+ doPurgeConnection = true;
+ // (A potential problem of this program logic is that setting the doPurgeConnection flag
+ // has an effect only if the JDBC driver calls connectionClosed() synchronously within
+ // Connection.close().)
+ conn.close(); }
+ catch (SQLException e) {}
+ // ignore exception from close()
+ finally {
+ doPurgeConnection = false; }}
+
+private synchronized void recycleConnection (PooledConnection pconn) {
+ if (isDisposed || doPurgeConnection) {
+ disposeConnection(pconn);
+ return; }
+ if (activeConnections <= 0) {
+ throw new AssertionError(); }
+ activeConnections--;
+ semaphore.release();
+ recycledConnections.add(pconn);
+ assertInnerState(); }
+
+private synchronized void disposeConnection (PooledConnection pconn) {
+ pconn.removeConnectionEventListener(poolConnectionEventListener);
+ if (!recycledConnections.remove(pconn)) {
+ // If the PooledConnection is not in the recycledConnections list,
+ // we assume that the connection was active.
+ if (activeConnections <= 0) {
+ throw new AssertionError(); }
+ activeConnections--;
+ semaphore.release(); }
+ closeConnectionAndIgnoreException(pconn);
+ assertInnerState(); }
+
+private void closeConnectionAndIgnoreException (PooledConnection pconn) {
+ try {
+ pconn.close(); }
+ catch (SQLException e) {
+ log("Error while closing database connection: "+e.toString()); }}
+
+private void log (String msg) {
+ String s = "MiniConnectionPoolManager: "+msg;
+ try {
+ if (logWriter == null) {
+ System.err.println(s); }
+ else {
+ logWriter.println(s); }}
+ catch (Exception e) {}}
+
+private void assertInnerState() {
+ if (activeConnections < 0) {
+ throw new AssertionError(); }
+ if (activeConnections + recycledConnections.size() > maxConnections) {
+ throw new AssertionError(); }
+ if (activeConnections + semaphore.availablePermits() > maxConnections) {
+ throw new AssertionError(); }}
+
+private class PoolConnectionEventListener implements ConnectionEventListener {
+ public void connectionClosed (ConnectionEvent event) {
+ PooledConnection pconn = (PooledConnection)event.getSource();
+ recycleConnection(pconn); }
+ public void connectionErrorOccurred (ConnectionEvent event) {
+ PooledConnection pconn = (PooledConnection)event.getSource();
+ disposeConnection(pconn); }}
+
+/**
+* Returns the number of active (open) connections of this pool.
+*
+* Connection
objects that have been
+* issued by {@link #getConnection()}, for which Connection.close()
+* has not yet been called.
+*
+* @return
+* the number of active connections.
+**/
+public synchronized int getActiveConnections() {
+ return activeConnections; }
+
+/**
+* Returns the number of inactive (unused) connections in this pool.
+*
+* Connection.close()
has been called and which
+* have not yet been reused.
+*
+* @return
+* the number of inactive connections.
+**/
+public synchronized int getInactiveConnections() {
+ return recycledConnections.size(); }
+
+} // end class MiniConnectionPoolManager
\ No newline at end of file
diff --git a/src/uk/org/whoami/authme/datasource/MySQLDataSource.java b/src/uk/org/whoami/authme/datasource/MySQLDataSource.java
index dd249ed..b4d218e 100644
--- a/src/uk/org/whoami/authme/datasource/MySQLDataSource.java
+++ b/src/uk/org/whoami/authme/datasource/MySQLDataSource.java
@@ -16,13 +16,13 @@
package uk.org.whoami.authme.datasource;
+import com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource;
import java.sql.Connection;
-import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
-import java.sql.Timestamp;
+import java.util.Date;
import java.util.HashMap;
import uk.org.whoami.authme.ConsoleLogger;
import uk.org.whoami.authme.cache.auth.PlayerAuth;
@@ -40,7 +40,7 @@ public class MySQLDataSource implements DataSource {
private String columnPassword;
private String columnIp;
private String columnLastLogin;
- private Connection con;
+ private MiniConnectionPoolManager conPool;
public MySQLDataSource() throws ClassNotFoundException, SQLException {
Settings s = Settings.getInstance();
@@ -63,23 +63,30 @@ public MySQLDataSource() throws ClassNotFoundException, SQLException {
private synchronized void connect() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
ConsoleLogger.info("MySQL driver loaded");
- con = DriverManager.getConnection("jdbc:mysql://" + host + ":" + port
- + "/" + database, username, password);
- ConsoleLogger.info("Connected to MySQL");
+ MysqlConnectionPoolDataSource dataSource = new MysqlConnectionPoolDataSource();
+ dataSource.setDatabaseName(database);
+ dataSource.setServerName(host);
+ dataSource.setPort(Integer.parseInt(port));
+ dataSource.setUser(username);
+ dataSource.setPassword(password);
+
+ conPool = new MiniConnectionPoolManager(dataSource, 10);
+ ConsoleLogger.info("Connection pool ready");
}
private synchronized void setup() throws SQLException {
-
+ Connection con = null;
Statement st = null;
ResultSet rs = null;
try {
+ con = conPool.getConnection();
st = con.createStatement();
st.executeUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " ("
+ "id INTEGER AUTO_INCREMENT,"
+ columnName + " VARCHAR(255) NOT NULL,"
+ columnPassword + " VARCHAR(255) NOT NULL,"
+ columnIp + " VARCHAR(40) NOT NULL,"
- + columnLastLogin + " TIMESTAMP,"
+ + columnLastLogin + " BIGINT,"
+ "CONSTRAINT table_const_prim PRIMARY KEY (id));");
rs = con.getMetaData().getColumns(null, null, tableName, columnIp);
@@ -91,61 +98,55 @@ private synchronized void setup() throws SQLException {
rs = con.getMetaData().getColumns(null, null, tableName, columnLastLogin);
if (!rs.next()) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
- + columnLastLogin + " TIMESTAMP;");
+ + columnLastLogin + " BIGINT;");
}
} finally {
- if (st != null) {
- try {
- st.close();
- } catch (SQLException ex) {
- }
- }
- if (rs != null) {
- try {
- rs.close();
- } catch (SQLException ex) {
- }
- }
+ close(rs);
+ close(st);
+ close(con);
}
ConsoleLogger.info("MySQL Setup finished");
}
@Override
public synchronized boolean isAuthAvailable(String user) {
+ Connection con = null;
PreparedStatement pst = null;
+ ResultSet rs = null;
try {
+ con = conPool.getConnection();
pst = con.prepareStatement("SELECT * FROM " + tableName + " WHERE "
+ columnName + "=?;");
pst.setString(1, user);
- ResultSet rs = pst.executeQuery();
+ rs = pst.executeQuery();
return rs.next();
} catch (SQLException ex) {
ConsoleLogger.showError(ex.getMessage());
return false;
} finally {
- if (pst != null) {
- try {
- pst.close();
- } catch (SQLException ex) {
- }
- }
+ close(rs);
+ close(pst);
+ close(con);
}
}
@Override
public synchronized PlayerAuth getAuth(String user) {
+ Connection con = null;
PreparedStatement pst = null;
+ ResultSet rs = null;
try {
+ con = conPool.getConnection();
pst = con.prepareStatement("SELECT * FROM " + tableName + " WHERE "
+ columnName + "=?;");
pst.setString(1, user);
- ResultSet rs = pst.executeQuery();
+ rs = pst.executeQuery();
if (rs.next()) {
if (rs.getString(columnIp).isEmpty()) {
- return new PlayerAuth(rs.getString(columnName), rs.getString(columnPassword), "198.18.0.1", rs.getTimestamp(columnLastLogin));
+ return new PlayerAuth(rs.getString(columnName), rs.getString(columnPassword), "198.18.0.1", new Date(rs.getLong(columnLastLogin)));
} else {
- return new PlayerAuth(rs.getString(columnName), rs.getString(columnPassword), rs.getString(columnIp), rs.getTimestamp(columnLastLogin));
+ return new PlayerAuth(rs.getString(columnName), rs.getString(columnPassword), rs.getString(columnIp), new Date(rs.getLong(columnLastLogin)));
}
} else {
return null;
@@ -154,43 +155,40 @@ public synchronized PlayerAuth getAuth(String user) {
ConsoleLogger.showError(ex.getMessage());
return null;
} finally {
- if (pst != null) {
- try {
- pst.close();
- } catch (SQLException ex) {
- }
- }
+ close(rs);
+ close(pst);
+ close(con);
}
}
@Override
public synchronized boolean saveAuth(PlayerAuth auth) {
+ Connection con = null;
PreparedStatement pst = null;
try {
+ con = conPool.getConnection();
pst = con.prepareStatement("INSERT INTO " + tableName + "(" + columnName + "," + columnPassword + "," + columnIp + "," + columnLastLogin + ") VALUES (?,?,?,?);");
pst.setString(1, auth.getNickname());
pst.setString(2, auth.getHash());
pst.setString(3, auth.getIp());
- pst.setTimestamp(4, new Timestamp(auth.getLastLogin().getTime()));
+ pst.setLong(4, auth.getLastLogin().getTime());
pst.executeUpdate();
} catch (SQLException ex) {
ConsoleLogger.showError(ex.getMessage());
return false;
} finally {
- if (pst != null) {
- try {
- pst.close();
- } catch (SQLException ex) {
- }
- }
+ close(pst);
+ close(con);
}
return true;
}
@Override
public synchronized boolean updatePassword(PlayerAuth auth) {
+ Connection con = null;
PreparedStatement pst = null;
try {
+ con = conPool.getConnection();
pst = con.prepareStatement("UPDATE " + tableName + " SET " + columnPassword + "=? WHERE " + columnName + "=?;");
pst.setString(1, auth.getHash());
pst.setString(2, auth.getNickname());
@@ -199,43 +197,39 @@ public synchronized boolean updatePassword(PlayerAuth auth) {
ConsoleLogger.showError(ex.getMessage());
return false;
} finally {
- if (pst != null) {
- try {
- pst.close();
- } catch (SQLException ex) {
- }
- }
+ close(pst);
+ close(con);
}
return true;
}
@Override
public boolean updateLogin(PlayerAuth auth) {
+ Connection con = null;
PreparedStatement pst = null;
try {
+ con = conPool.getConnection();
pst = con.prepareStatement("UPDATE " + tableName + " SET " + columnIp + "=?, " + columnLastLogin + "=? WHERE " + columnName + "=?;");
pst.setString(1, auth.getIp());
- pst.setTimestamp(2, new Timestamp(auth.getLastLogin().getTime()));
+ pst.setLong(2, auth.getLastLogin().getTime());
pst.setString(3, auth.getNickname());
pst.executeUpdate();
} catch (SQLException ex) {
ConsoleLogger.showError(ex.getMessage());
return false;
} finally {
- if (pst != null) {
- try {
- pst.close();
- } catch (SQLException ex) {
- }
- }
+ close(pst);
+ close(con);
}
return true;
}
@Override
public synchronized boolean removeAuth(String user) {
+ Connection con = null;
PreparedStatement pst = null;
try {
+ con = conPool.getConnection();
pst = con.prepareStatement("DELETE FROM " + tableName + " WHERE " + columnName + "=?;");
pst.setString(1, user);
pst.executeUpdate();
@@ -243,12 +237,8 @@ public synchronized boolean removeAuth(String user) {
ConsoleLogger.showError(ex.getMessage());
return false;
} finally {
- if (pst != null) {
- try {
- pst.close();
- } catch (SQLException ex) {
- }
- }
+ close(pst);
+ close(con);
}
return true;
}
@@ -256,43 +246,66 @@ public synchronized boolean removeAuth(String user) {
@Override
public synchronized HashMap