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
+* 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 LinkedList recycledConnections; +private int activeConnections; +private PoolConnectionEventListener poolConnectionEventListener; +private boolean isDisposed; +private boolean doPurgeConnection; + +/** +* Thrown in {@link #getConnection()} or {@link #getValidConnection()} when no free connection becomes +* available within timeout 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 LinkedList(); + poolConnectionEventListener = new PoolConnectionEventListener(); } + +/** +* Closes all unused pooled connections. +*/ +public synchronized void dispose() throws SQLException { + if (isDisposed) { + return; } + isDisposed = true; + SQLException e = null; + while (!recycledConnections.isEmpty()) { + PooledConnection pconn = recycledConnections.remove(); + try { + pconn.close(); } + catch (SQLException e2) { + if (e == null) { + e = e2; }}} + if (e != null) { + throw e; }} + +/** +* Retrieves a connection from the connection pool. +* +*

If maxConnections 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)}. +* +*

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 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. +* +*

This is the number of 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. +* +*

This is the number of internally kept recycled connections, +* for which 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 getAllRegisteredUsers() { HashMap map = new HashMap(); + Connection con = null; PreparedStatement pst = null; + ResultSet rs = null; try { + con = conPool.getConnection(); pst = con.prepareStatement("SELECT * FROM " + tableName + ";"); - ResultSet rs = pst.executeQuery(); + rs = pst.executeQuery(); while (rs.next()) { if (rs.getString(columnIp).isEmpty()) { - map.put(rs.getString(columnName), new PlayerAuth(rs.getString(columnName), rs.getString(columnPassword), "198.18.0.1", rs.getTimestamp(columnLastLogin))); + map.put(rs.getString(columnName), new PlayerAuth(rs.getString(columnName), rs.getString(columnPassword), "198.18.0.1", new Date(rs.getLong(columnLastLogin)))); } else { - map.put(rs.getString(columnName), new PlayerAuth(rs.getString(columnName), rs.getString(columnPassword), rs.getString(columnIp), rs.getTimestamp(columnLastLogin))); + map.put(rs.getString(columnName), new PlayerAuth(rs.getString(columnName), rs.getString(columnPassword), rs.getString(columnIp), new Date(rs.getLong(columnLastLogin)))); } } } catch (SQLException ex) { ConsoleLogger.showError(ex.getMessage()); return map; } finally { - if (pst != null) { - try { - pst.close(); - } catch (SQLException ex) { - } - } + close(rs); + close(pst); + close(con); } return map; } @Override public synchronized void close() { - if (con != null) { + } + + @Override + public void reload() { + } + + private void close(Statement st) { + if (st != null) { try { - con.close(); + st.close(); } catch (SQLException ex) { - ConsoleLogger.showError("Couldn't close MySQL connection"); + ConsoleLogger.showError(ex.getMessage()); } } } - @Override - public void reload() { + private void close(ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException ex) { + ConsoleLogger.showError(ex.getMessage()); + } + } + } + + private void close(Connection con) { + if (con != null) { + try { + con.close(); + } catch (SQLException ex) { + ConsoleLogger.showError(ex.getMessage()); + } + } } }