/*
 * 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@ricochet.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2001-2004 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.db;

import java.util.*;
import java.sql.*;
import javax.naming.*;
import javax.sql.*;
import org.apache.log4j.*;
import org.w3c.dom.*;
import com.silverwrist.util.DOMElementHelper;
import com.silverwrist.venice.except.ConfigException;

/**
 * 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 DataPool implements Runnable
{
  /*--------------------------------------------------------------------------------
   * Static data values
   *--------------------------------------------------------------------------------
   */

  private static Category logger = Category.getInstance(DataPool.class);

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

  private DataSource m_dsource = null; // JCA DataSource (overrides everything else)
  private String driver;               // name of JDBC driver class
  private String url;                  // URL for JDBC connection
  private String username;             // username for JDBC connection
  private String password;             // password for JDBC connection
  private int max_conns;               // maximum number of possible connections
  private boolean wait_if_busy;        // do we wait for a connection if none is available?
  private boolean pending = false;     // pending connection being created?
  private Vector avail_connections;    // connections which are available for use
  private Vector busy_connections;     // connections which are currently in use
  private int thread_counter = 1;      // counter for data pool threads

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

  /**
   * Creates and configures a new database pool, based on a section from the Venice configuration file.
   *
   * @param cfg The &lt;database/&gt; section of the configuration file.
   * @exception com.silverwrist.venice.core.ConfigException The configuration is not correct in some way.
   * @exception java.sql.SQLException If the initial connections could not be created.
   */
  public DataPool(Element cfg) throws ConfigException, SQLException
  {
    // Make sure they passed the <database/> section to us.
    if (!(cfg.getTagName().equals("database")))
    { // not the right section name
      logger.fatal("configuration section is not <database/>");
      throw new ConfigException("improper configuration section",cfg);

    } // end if

    // Use a DOMElementHelper to read off the configuration for the DataPool.
    DOMElementHelper cfgx = new DOMElementHelper(cfg);
    String dsname = cfgx.getSubElementText("jca-datasource");
    if (dsname!=null)
    { // they specified a JCA DataSource name - try to bind to it
      if (logger.isDebugEnabled())
	logger.debug("DataPool JCA data source: " + dsname);
      try
      { // get the datasource
	Context ctx = new InitialContext();
	m_dsource = (DataSource)(ctx.lookup("java:/" + dsname));
	if (m_dsource!=null)
	  return;  // initialization is effectively done
	else
	  logger.warn("No data source found with name \"" + dsname + "\"; trying regular initialization");

      } // end try
      catch (NamingException e)
      { // output an error message
	logger.warn("Error getting data source \"" + dsname + "\"; trying regular initialization",e);
	m_dsource = null;

      } // end catch

    } // end if
    else
      logger.info("no JCA data source name configured - init the old way");

    // proceed with regular initialization
    driver = cfgx.getSubElementText("driver");
    if (driver==null)
    { // no driver found!
      logger.fatal("no <driver/> name inside <database/> section");
      throw new ConfigException("no <driver/> name specified",cfg);

    } // end if

    if (logger.isDebugEnabled())
      logger.debug("DataPool driver: " + driver);

    url = cfgx.getSubElementText("uri");
    if (url==null)
    { // no database URI found!
      logger.fatal("no <uri/> name inside <database/> section");
      throw new ConfigException("no database <uri/> specified",cfg);

    } // end if

    if (logger.isDebugEnabled())
      logger.debug("DataPool URI: " + url);

    username = cfgx.getSubElementText("username");
    if (username==null)
    { // no database username found
      logger.fatal("no <username/> inside <database/> section");
      throw new ConfigException("no database <username/> specified",cfg);

    } // end if

    if (logger.isDebugEnabled())
      logger.debug("DataPool username: " + url);

    password = cfgx.getSubElementText("password");
    if (password==null)
    { // no database password found
      logger.fatal("no user <password/> inside <database/> section");
      throw new ConfigException("no database <password/> specified",cfg);

    } // end if

    int initial_conns = 0;
    try
    { // retrieve the initial-conns figure and make it an integer
      String tmp = cfgx.getSubElementText("initial-conns");
      if (tmp==null)
      { // the connections value was not specified
	logger.fatal("database <initial-conns/> value not specified");
	throw new ConfigException("no <initial-conns/> value specified",cfg);

      } // end if

      initial_conns = Integer.parseInt(tmp);
      if (initial_conns<0)
      { // the connections value was out of range
	logger.fatal("database <initial-conns/> value was out of range");
	throw new ConfigException("<initial-conns/> value out of range",cfg);

      } // end if

    } // end try
    catch (NumberFormatException e)
    { // the value wasn't a valid integer
      logger.fatal("database <initial-conns/> value was not a number");
      throw new ConfigException("<initial-conns/> value is not a number",e,cfg);

    } // end catch

    if (logger.isDebugEnabled())
      logger.debug("DataPool initial connections: " + initial_conns);

    try
    { // retrieve the max-conns figure and make it an integer
      String tmp = cfgx.getSubElementText("max-conns");
      if (tmp==null)
      { // maximum connections value not specified
	logger.fatal("database <max-conns/> value not specified");
	throw new ConfigException("no <max-conns/> value specified",cfg);

      } // end if

      max_conns = Integer.parseInt(tmp);
      if (max_conns<=0)
      { // value must be greater than 0!
	logger.fatal("database <max-conns/> value was out of range");
	throw new ConfigException("<max-conns/> value out of range",cfg);

      } // end if

    } // end try
    catch (NumberFormatException e)
    { // the value wasn't a valid integer
      logger.fatal("database <max-conns/> value was not a number");
      throw new ConfigException("<max-conns/> value is not a number",e,cfg);

    } // end catch

    if (logger.isDebugEnabled())
      logger.debug("DataPool maximum connections: " + max_conns);

    wait_if_busy = cfgx.hasChildElement("wait-if-busy");
    if (logger.isDebugEnabled())
      logger.debug("DataPool wait if busy: " + String.valueOf(wait_if_busy));

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

    } // end if

    // Create the vectors that hold connections.
    avail_connections = new Vector(initial_conns);
    busy_connections = new Vector();

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

    logger.info("DataPool initialized");

  } // end constructor

  /*--------------------------------------------------------------------------------
   * finalize() function
   *--------------------------------------------------------------------------------
   */

  /**
   * Called by the garbage collector on an object when garbage collection determines that there are no
   * more references to the object.
   */
  protected void finalize()
  {
    closeConnections(avail_connections);
    closeConnections(busy_connections);

  } // end finalize

  /*--------------------------------------------------------------------------------
   * Internal functions
   *--------------------------------------------------------------------------------
   */

  /**
   * Creates and returns a new database connection.
   *
   * @return The new database connection object.
   * @exception java.sql.SQLException An error prevented the creation of the connection.
   */
  private final Connection makeNewConnection() throws SQLException
  {
    try
    { // make the connection and return it
      Class.forName(driver).newInstance();  // preload driver class
      // the newInstance() call above is to work around some broken Java implementations
      return DriverManager.getConnection(url,username,password);

    } // end try
    catch (ClassNotFoundException e)
    { // convert a ClassNotFoundException to a nicer SQLException
      logger.error("JDBC driver class \"" + driver + "\" not found",e);
      throw new SQLException("Can't find class for driver: " + driver);

    } // end catch
    catch (IllegalAccessException e2)
    { // convert this to a nicer SQLException
      logger.fatal("Can't access \"" + driver + "\" constructor: " + e2.getMessage(),e2);
      throw new SQLException("Can't access class for driver: " + driver);

    } // end catch
    catch (InstantiationException e3)
    { // convert this to a nicer SQLException
      logger.fatal("Can't create instance of \"" + driver + "\": " + e3.getMessage(),e3);
      throw new SQLException("Can't create class for driver: " + driver);

    } // end catch

  } // end makeNewConnection

  /**
   * Spins off a background thread to create a new database connection.
   *
   * @see #run()
   */
  private final void makeBackgroundConnection()
  {
    pending = true;
    try
    { // spin off the connection attempt to the background
      Thread thrd = new Thread(this,"DataPool_bkgd_" + (thread_counter++));
      thrd.start();

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

    } // end catch

  } // end makeBackgroundConnection

  /**
   * Closes all database connections in the specified vector.
   *
   * @param vconn Vector of connections to be closed.
   */
  private final static 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

  /*--------------------------------------------------------------------------------
   * Implementations from interface Runnable
   *--------------------------------------------------------------------------------
   */

  /**
   * The thread function which creates a new connection and adds it to the list of available
   * connections "in the background."  It then notifies all threads which are waiting for a
   * new connection.
   *
   * @see #makeBackgroundConnection()
   * @see #makeNewConnection()
   */
  public void run()
  {
    try
    { // attempt to create another connection in the background
      Connection c = makeNewConnection();
      synchronized (this)
      { // we've got a new available connection - let everyone know
	if (logger.isDebugEnabled())
	  logger.debug("created new connection in the background");
	avail_connections.addElement(c);
	pending = false;
	notifyAll();

      } // end synchronized block

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

    } // end catch

  } // end run

  /*--------------------------------------------------------------------------------
   * External operations
   *--------------------------------------------------------------------------------
   */

  /**
   * Returns the number of connections currently managed by this pool.
   *
   * @return The number of connections currently managed by this pool.
   */
  public synchronized int numConnections()
  {
    if (m_dsource!=null)
      return 1;
    int rc = avail_connections.size() + busy_connections.size();
    if (logger.isDebugEnabled())
      logger.debug("numConnections() => " + rc);
    return rc;

  } // end numConnections

  /**
   * Attempts to get a database connection from the pool, waiting for one if the "wait-if-busy" flag was set
   * in the data pool's configuration.
   *
   * @return A new database connection.
   * @exception java.sql.SQLException The connection limit was reached, or an error prevented the creation
   *            of another connection.
   */
  public synchronized Connection getConnection() throws SQLException
  {
    if (m_dsource!=null)
      return m_dsource.getConnection();

    for (;;)
    { // loop until we get a connection or throw an exception
      if (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() < max_conns) && !pending)
	  makeBackgroundConnection();  // try to create a new connection
	else if (!wait_if_busy)
	{ // don't want to wait?  tough, we're h0sed!
	  logger.error("exhausted maximum connection limit (" + max_conns + ")");
	  throw new SQLException("connection limit reached");

	} // 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 = avail_connections.size() - 1;
	Connection rc = (Connection)(avail_connections.elementAt(ndx));
	avail_connections.removeElementAt(ndx);
	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
	  busy_connections.addElement(rc);
	  return new WrappedConnection(this,rc);

	} // end else

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

    } // end for (ever)

  } // end getConnection

  /**
   * Returns a database connection to the pool.
   *
   * @param c The connection to be returned to the pool.
   */
  synchronized void releaseConnection(Connection c)
  {
    if (m_dsource!=null)
    { // just close the connection
      try
      { // close the connection
	c.close();

      } // end try
      catch (SQLException e)
      { // log exception and continue
	logger.error("c.close() exception",e);

      } // end catch

      return;

    } // end if

    if (c!=null)
    { // can release this connection
      if (c instanceof WrappedConnection)
      { // close our wrapped connection
	try
	{ // close the connection
	  c.close();

	} // end try
	catch (SQLException e)
	{ // log exception and continue
	  logger.error("c.close() exception",e);

	} // end catch

	return;

      } // end if

      // move from one vector to another
      busy_connections.removeElement(c);
      avail_connections.addElement(c);
      notifyAll();  // wake up!  Got a new connection for you!

    } // end if

  } // end releaseConnection

  /**
   * Closes all connections managed by this data pool.
   *
   * @see #closeConnections(java.util.Vector)
   */
  public synchronized void closeAllConnections()
  {
    if (m_dsource!=null)
      return;
    if (logger.isDebugEnabled())
      logger.debug("closing ALL connections!");
    closeConnections(avail_connections);
    avail_connections = new Vector();
    closeConnections(busy_connections);
    busy_connections = new Vector();

  } // end closeAllConnections

  /**
   * Returns a string reflecting the current state of the data pool, commonly used for debugging.
   *
   * @return The debugging string for this pool.
   */
  public synchronized String toString()
  {
    if (m_dsource!=null)
      return "DataPool " + m_dsource.toString();
    StringBuffer info = new StringBuffer();
    info.append("DataPool(\"").append(url).append("\",\"").append(username).append("\"), ");
    info.append(avail_connections.size()).append(" avail, ").append(busy_connections.size());
    info.append(" busy, ").append(max_conns).append(" max");
    return info.toString();

  } // end toString

} // end class DataPool
