/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2002-03 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.dynamo.db;

import java.sql.*;
import java.util.*;
import org.apache.log4j.Logger;
import org.w3c.dom.*;
import com.silverwrist.util.xml.*;
import com.silverwrist.dynamo.Namespaces;
import com.silverwrist.dynamo.except.*;
import com.silverwrist.dynamo.iface.*;

/**
 * A simple pooling system for JDBC connections, which allows connections to be kept in a pool until
 * they're needed, avoiding the overhead of opening and closing connections for each transaction, or
 * keeping one database connection open for each user.<P>
 * Based on some code from Marty Hall's <EM>Core Servlets and Java Server Pages</EM> (Prentice Hall/
 * Sun Microsystems, 2000).
 *
 * @author Eric J. Bowersox &lt;erbo@silcom.com&gt;
 * @version X
 */
public class DatabaseConnectionPool implements DBConnectionPool, ComponentInitialize, ComponentShutdown
{
  /*--------------------------------------------------------------------------------
   * Internal background thread object
   *--------------------------------------------------------------------------------
   */

  private class BackgroundThread extends Thread
  {
    /*====================================================================
     * Constructor
     *====================================================================
     */

    BackgroundThread()
    {
      super("DatabaseConnectionPool_bkgd_" + (s_thread_counter++));

    } // end constructor

    /*====================================================================
     * Overrides from class Thread
     *====================================================================
     */

    public void run()
    {
      try
      { // attempt to create another connection in the background
	Connection c = makeNewConnection();
	synchronized (DatabaseConnectionPool.this)
	{ // we've got a new available connection - let everyone know
	  if (logger.isDebugEnabled())
	    logger.debug("created new connection in the background");
	  m_avail_connections.addElement(c);
	  m_pending = false;
	  DatabaseConnectionPool.this.notifyAll();

	} // end synchronized block

      } // end try
      catch (Exception e)
      { // caught an SQLException or an OutOfMemory exception
	logger.warn("background connection thread caught exception",e);
	// ditch this new connection and wait until an existing one frees up

      } // end catch

    } // end run

  } // end BackgroundThread

  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static Logger logger = Logger.getLogger(DatabaseConnectionPool.class);

  private static final Map INFER_TYPE_MAP;      // used to infer database type from JDBC driver classname

  private static int s_thread_counter = 1;      // counter for data pool threads

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private String m_name;                        // name of connection pool
  private String m_dbtype;                      // database type
  private String m_driver;                      // name of JDBC driver class
  private String m_url;                         // URL for JDBC connection
  private String m_username;                    // username for JDBC connection
  private String m_password;                    // password for JDBC connection
  private int m_max_conns;                      // maximum number of possible connections
  private boolean m_wait_if_busy;               // do we wait for a connection if none is available?
  private boolean m_pending = false;            // pending connection being created?
  private Vector m_avail_connections = null;    // connections which are available for use
  private Vector m_busy_connections = null;     // connections which are currently in use
  private DBUtilities m_util_object;            // utilities object
  private PropertySerializer m_psz;             // property serializer object

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  /**
   * Creates a new <CODE>DatabaseCOnnectionPool</CODE> object.  Commonly called at initialization time,
   * when the objects are created from the Dynamo XML configuration file.
   */
  public DatabaseConnectionPool()
  { // do nothing
  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal operations
   *--------------------------------------------------------------------------------
   */

  /**
   * Creates a standard database exception.
   *
   * @param message The message ID from the "DatabaseMessages" bundle to load.
   * @return The new exception object.
   */
  private static final DatabaseException newException(String message)
  {
    return new DatabaseException(DatabaseConnectionPool.class,"DatabaseMessages",message);

  } // end newException

  /**
   * Creates a standard database exception.
   *
   * @param message The message ID from the "DatabaseMessages" bundle to load.
   * @param inner The exception to wrap in the new
   *              {@link com.silverwrist.dynamo.except.DatabaseException DatabaseException} object.
   * @return The new exception object.
   */
  private static final DatabaseException newException(String message, Throwable inner)
  {
    return new DatabaseException(DatabaseConnectionPool.class,"DatabaseMessages",message,inner);

  } // end newException

  /**
   * Closes all database connections in the specified vector.
   *
   * @param vconn Vector of connections to be closed.
   */
  private static final void closeConnections(Vector vconn)
  {
    if (logger.isDebugEnabled())
      logger.debug("closeConnections(" + vconn.size() + " to be closed)");

    for (int i=0; i<vconn.size(); i++)
    { // loop over the entire vector
      try
      { // take each connection and close it
	Connection c = (Connection)(vconn.get(i));
	if (!(c.isClosed()))
	  c.close();

      } // end try
      catch (SQLException e)
      { // what do we care, this connection is dying anyhow
	logger.warn("got a SQLException in closeConnections (ignored): " + e.getMessage());

      } // end catch

    } // end for

  } // end closeConnections

  /**
   * Creates and returns a new database connection.
   *
   * @return The new database connection object.
   * @exception com.silverwrist.dynamo.except.DatabaseException An error prevented the creation
   *            of the connection.
   */
  private final Connection makeNewConnection() throws DatabaseException
  {
    if (m_busy_connections==null)
      throw newException("dbx.notInitialized");
      
    try
    { // make the connection and return it
      Class.forName(m_driver).newInstance();  // preload driver class
      // the newInstance() call above is to work around some broken Java implementations
      return DriverManager.getConnection(m_url,m_username,m_password);

    } // end try
    catch (ClassNotFoundException e)
    { // convert a ClassNotFoundException to a nicer DatabaseException
      logger.error("JDBC driver class \"" + m_driver + "\" not found",e);
      DatabaseException d = newException("dbx.classNotFound");
      d.setParameter(0,m_driver);
      throw d;

    } // end catch
    catch (IllegalAccessException e2)
    { // convert this to a nicer DatabaseException
      logger.fatal("Can't access \"" + m_driver + "\" constructor",e2);
      DatabaseException d = newException("dbx.illegalAccess");
      d.setParameter(0,m_driver);
      throw d;

    } // end catch
    catch (InstantiationException e3)
    { // convert this to a nicer DatabaseException
      logger.fatal("Can't create instance of \"" + m_driver + "\"",e3);
      DatabaseException d = newException("dbx.instantiation");
      d.setParameter(0,m_driver);
      throw d;

    } // end catch
    catch (SQLException e4)
    { // convert this to a DatabaseException, too
      logger.fatal("Can't create database connection",e4);
      throw newException("dbx.sqlMakeNewConn",e4);

    } // end catch

  } // end makeNewConnection

  /**
   * Spins off a background thread to create a new database connection.
   */
  private final void makeBackgroundConnection()
  {
    m_pending = true;
    try
    { // spin off the connection attempt to the background
      BackgroundThread thrd = new BackgroundThread();
      thrd.start();

    } // end try
    catch (OutOfMemoryError e)
    { // give up on new connection
      logger.warn("memory failure spinning off background-connect thread");
      m_pending = false;

    } // end catch

  } // end makeBackgroundConnection

  /*--------------------------------------------------------------------------------
   * Implementations from interface NamedObject
   *--------------------------------------------------------------------------------
   */

  /**
   * Returns the name of this object.
   *
   * @return The name of this object.
   */
  public String getName()
  {
    return m_name;

  } // end getName

  /*--------------------------------------------------------------------------------
   * Implementations from interface ServiceProvider
   *--------------------------------------------------------------------------------
   */

  /**
   * Queries this object for a specified service.
   *
   * @param klass The class of the object that should be returned as a service.
   * @return A service object.  The service object is guaranteed to be of the class
   *         specified by <CODE>klass</CODE>; that is, if <CODE>queryService(klass)</CODE>
   *         yields some object <CODE>x</CODE>, then the expression <CODE>klass.isInstance(x)</CODE>
   *         is true.
   * @exception com.silverwrist.dynamo.except.NoSuchServiceException If no service is available in
   *            the specified class.
   */
  public Object queryService(Class klass)
  {
    if (klass==DBUtilities.class)
      return m_util_object;
    if (klass==PropertySerializer.class)
      return m_psz;
    if (klass==DBConnectionPool.class)
      return (DBConnectionPool)this;
    throw new NoSuchServiceException("DatabaseConnectionPool",klass);

  } // end queryService

  /**
   * Queries this object for a specified service.
   *
   * @param klass The class of the object that should be returned as a service.
   * @param serviceid ID for the service to be requested, to further discriminate between requests.
   * @return A service object.  The service object is guaranteed to be of the class
   *         specified by <CODE>klass</CODE>; that is, if <CODE>queryService(klass)</CODE>
   *         yields some object <CODE>x</CODE>, then the expression <CODE>klass.isInstance(x)</CODE>
   *         is true.
   * @exception com.silverwrist.dynamo.except.NoSuchServiceException If no service is available in
   *            the specified class.
   */
  public Object queryService(Class klass, String serviceid)
  {
    return this.queryService(klass);

  } // end queryService

  /*--------------------------------------------------------------------------------
   * Implementations from interface DBConnectionPool
   *--------------------------------------------------------------------------------
   */

  /**
   * Returns a simple string identifying the database type.  This is commonly used in database operations
   * classes to create classnames that are dynamically loaded.
   *
   * @return The database identifier.
   */
  public String getDatabaseType()
  {
    return m_dbtype;

  } // end getDatabaseType

  /**
   * Returns the number of connections currently being managed by this connection pool.
   *
   * @return The number of connections currently being managed by this connection pool.
   */
  public int numConnections()
  {
    if (m_busy_connections==null)
      return 0;
    int rc = m_avail_connections.size() + m_busy_connections.size();
    if (logger.isDebugEnabled())
      logger.debug("numConnections() => " + rc);
    return rc;

  } // end numConnections

  /**
   * Obtains a database connection object from the connection pool.
   *
   * @return The <CODE>Connection</CODE> object.
   * @exception com.silverwrist.dynamo.except.DatabaseException If a connection could not be obtained.
   */
  public synchronized Connection getConnection() throws DatabaseException
  {
    if (m_busy_connections==null)
      throw newException("dbx.notInitialized");
      
    for (;;)
    { // loop until we get a connection or throw an exception
      if (m_avail_connections.isEmpty())
      { // no connections available - we may need to make a new one
	if (logger.isDebugEnabled())
	  logger.debug("no connections available - looking to get one");
	if ((numConnections() < m_max_conns) && !m_pending)
	  makeBackgroundConnection();  // try to create a new connection
	else if (!m_wait_if_busy)
	{ // don't want to wait?  tough, we're h0sed!
	  logger.error("exhausted maximum connection limit (" + m_max_conns + ")");
	  throw newException("dbx.connectionLimit");

	} // end else if

	// Wait for the background connect attempt to finish, or for
	// someone to return a connection.
	try
	{ // park the thread here until we know what's up
	  wait();

	} // end try
	catch (InterruptedException e)
	{ // do nothing
	} // end catch

	// now fall through the loop and try again

      } // end if
      else
      { // pull the last connection off the available list...
	int ndx = m_avail_connections.size() - 1;
	Connection rc = (Connection)(m_avail_connections.elementAt(ndx));
	m_avail_connections.removeElementAt(ndx);
	try
	{ // see if this is a good connection or not...
	  if (rc.isClosed())
	  { // this connection is closed - discard it, and notify any waiters that a slot
	    // has opened up
	    if (logger.isDebugEnabled())
	      logger.debug("discarding closed connection");
	    notifyAll();
	    rc = null;
	    // fall out to the end of the loop and try again

	  } // end if
	  else
	  { // this connection is OK - return it
	    m_busy_connections.addElement(rc);
	    return rc;

	  } // end else

	} // end try
	catch (SQLException e)
	{ // if isClosed() throws a SQLException, the connection is somehow broken
	  logger.warn("connection is borked, discarding",e);
	  notifyAll();
	  rc = null;
	  // fall out to the end of the loop and try again

	} // end catch

      } // end else (at least one connection available)

    } // end for (ever)

  } // end getConnection

  /**
   * Returns a database connection object to the connection pool.
   *
   * @param conn The <CODE>Connection</CODE> object to be released.
   * @exception com.silverwrist.dynamo.except.DatabaseException If there was an error returning the connection.
   */
  public synchronized void releaseConnection(Connection conn) throws DatabaseException
  {
    if (m_busy_connections==null)
      throw newException("dbx.notInitialized");
      
    if (conn!=null)
    { // move from one vector to another
      m_busy_connections.removeElement(conn);
      m_avail_connections.addElement(conn);
      notifyAll();  // wake up!  Got a new connection for you!

    } // end if

  } // end releaseConnection

  /*--------------------------------------------------------------------------------
   * Implementations from interface ComponentInitialize
   *--------------------------------------------------------------------------------
   */

  /**
   * Initialize the database connection.
   *
   * @param config_root Pointer to the &lt;dbconnection/&gt; section of the Dynamo XML configuration file that
   *                    configures this database connection.  This is to be considered "read-only" by the component.
   * @param services An implementation of {@link com.silverwrist.dynamo.iface.ServiceProvider ServiceProvider}
   *                 which provides initialization services to the component.  This will include an implementation
   *                 of {@link com.silverwrist.dynamo.iface.ObjectProvider ObjectProvider} which may be used to
   *                 get information about other objects previously initialized by the application.
   * @exception com.silverwrist.dynamo.except.ConfigException If an error is encountered in the component
   *            configuration.
   */
  public void initialize(Element config_root, ServiceProvider services) throws ConfigException
  {
    XMLLoader loader = XMLLoader.get();
    try
    { // verify the right node name
      loader.verifyNodeName(config_root,"dbconnection");

      // get the object's name
      m_name = loader.getAttribute(config_root,"name");

      // get the various settings
      DOMElementHelper h = new DOMElementHelper(config_root);
      m_dbtype = h.getSubElementText("dbtype");
      if (logger.isDebugEnabled() && (m_dbtype!=null))
	logger.debug("DatabaseConnectionPool specified database type: " + m_dbtype);

      m_driver = loader.getSubElementText(h,"driver");
      if (logger.isDebugEnabled())
	logger.debug("DatabaseConnectionPool driver: " + m_driver);

      // If the database type is not known, try to infer it from the driver name.
      if (m_dbtype==null)
	m_dbtype = (String)(INFER_TYPE_MAP.get(m_driver));
      if (m_dbtype==null)
      { // wasn't specified, can't infer it - we are HOSED!
	ConfigException ce = new ConfigException(DatabaseConnectionPool.class,"DatabaseMessages",
						 "config.inferType");
	ce.setParameter(0,m_driver);
	throw ce;

      } // end if
      else if (logger.isDebugEnabled())
	logger.debug("DatabaseConnectionPool inferred database type: " + m_dbtype);

      m_url = loader.getSubElementText(h,"uri");
      if (logger.isDebugEnabled())
	logger.debug("DatabaseConnectionPool URI: " + m_url);

      m_username = loader.getSubElementText(h,"username");
      if (logger.isDebugEnabled())
	logger.debug("DatabaseConnectionPool user name: " + m_username);

      m_password = loader.getSubElementText(h,"password");

      Element elt = loader.getSubElement(h,"connections");
      int initial_conn = loader.getAttributeInt(elt,"initial",0);
      if (initial_conn<0)
	throw new ConfigException(DatabaseConnectionPool.class,"DatabaseMessages","config.initialConn");
      if (logger.isDebugEnabled())
	logger.debug("DatabaseConnectionPool initial connections: " + initial_conn);

      m_max_conns = loader.getAttributeInt(elt,"max");
      if (m_max_conns<0)
	throw new ConfigException(DatabaseConnectionPool.class,"DatabaseMessages","config.maxConn");
      if (logger.isDebugEnabled())
	logger.debug("DatabaseConnectionPool maximum connections: " + m_max_conns);

      m_wait_if_busy = loader.getAttributeBoolean(elt,"busywait",true);
      if (logger.isDebugEnabled())
	logger.debug("DatabaseConnectionPool wait if busy: " + m_wait_if_busy);

      if (initial_conn>m_max_conns)
      { // fix initial value if above maximum
	if (logger.isDebugEnabled())
	  logger.debug("N.B.: reducing configured initial connections");
	initial_conn = m_max_conns;

      } // end if
	
      try
      { // get the appropriate utilities object
	m_util_object = DBUtilitiesImpl.get(m_dbtype);

      } // end if
      catch (Exception e)
      { // the utilities are not implemented
	ConfigException ce = new ConfigException(DatabaseConnectionPool.class,"DatabaseMessages",
						 "utils.impl",e);
	ce.setParameter(0,m_dbtype);
	throw ce;

      } // end catch

      // Get the master property serializer so we can offer it as a service.
      m_psz = (PropertySerializer)(services.queryService(PropertySerializer.class));

      // Create the vectors that hold connections.
      m_avail_connections = new Vector(initial_conn);
      m_busy_connections = new Vector();

      // Populate the "available connection" vector.
      for (int i=0; i<initial_conn; i++)
	m_avail_connections.addElement(makeNewConnection());

      // See if we need to test the database.
      boolean do_test = false;
      elt = h.getSubElement("initialize");
      if (elt!=null)
	do_test = loader.getAttributeBoolean(elt,"test",false);
      if (do_test)
      { // database must be tested
	Connection conn = null;
	try
	{ // get connection and test it
	  conn = this.getConnection();
	  m_util_object.testDatabase(conn);

	} // end try
	catch (SQLException e)
	{ // translate SQL exception into ConfigException
	  ConfigException ce = new ConfigException(DatabaseConnectionPool.class,"DatabaseMessages","dbx.testfail",e);
	  ce.setParameter(0,m_name);
	  ce.setParameter(1,m_url);
	  ce.setParameter(2,e.getMessage());
	  throw ce;

	} // end catch
	finally
	{ // release the connection we got
	  this.releaseConnection(conn);

	} // end finally

      } // end if (testing database)

      logger.info("DatabaseConnectionPool initialized");

    } // end try
    catch (XMLLoadException e)
    { // error loading XML config data
      throw new ConfigException(e);

    } // end catch
    catch (DatabaseException e)
    { // error creating initial connections
      throw new ConfigException(DatabaseConnectionPool.class,"DatabaseMessages","config.create",e);

    } // end catch

  } // end initialize

  /*--------------------------------------------------------------------------------
   * Implementations from interface ComponentShutdown
   *--------------------------------------------------------------------------------
   */

  /**
   * Shuts down the database connection, closing all cached <CODE>Connection</CODE> objects.
   */
  public void shutdown()
  {
    closeConnections(m_avail_connections);
    m_avail_connections = null;
    closeConnections(m_busy_connections);
    m_busy_connections = null;

  } // end shutdown

  /*--------------------------------------------------------------------------------
   * Static initializer
   *--------------------------------------------------------------------------------
   */

  static
  { // Create the database type inference map.
    HashMap map = new HashMap();
    map.put("com.mysql.jdbc.Driver","mysql");
    map.put("org.gjt.mm.mysql.Driver","mysql");
    INFER_TYPE_MAP = Collections.unmodifiableMap(map);

  } // end static initializer

} // end class DatabaseConnectionPool
