// 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.
*
* <p>The public methods of this class are thread-safe.
*
* <p>Home page: <a href="http://www.source-code.biz/miniconnectionpoolmanager">www.source-code.biz/miniconnectionpoolmanager</a><br>
* Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland<br>
* 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<PooledConnection>   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 <code>timeout</code> 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<PooledConnection>();
   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.
*
* <p>If <code>maxConnections</code> connections are already in use, the method
* waits until a connection becomes available or <code>timeout</code> 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 <code>Connection</code> object.
* @throws TimeoutException
*    when no connection becomes available within <code>timeout</code> 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)}.
*
* <p>If a connection is not valid, the method tries to get another connection
* until one is valid (or a timeout occurs).
*
* <p>Pooled connections may become invalid when e.g. the database server is
* restarted.
*
* <p>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.
*
*
* <p>This method requires Java 1.6 or newer.
*
* @throws TimeoutException
*    when no valid connection becomes available within <code>timeout</code> 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.
*
* <p>This is the number of <code>Connection</code> objects that have been
* issued by {@link #getConnection()}, for which <code>Connection.close()</code>
* 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.
*
* <p>This is the number of internally kept recycled connections,
* for which <code>Connection.close()</code> 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