/***************************************************************************
    Copyright          : (C) 2002 by Neoworks Limited. All rights reserved
    URL                : http://www.neoworks.com
 ***************************************************************************/
/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

package com.neoworks.connectionpool;

import java.io.*;
import java.sql.*;
import java.net.*;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
import java.util.Properties;
import java.util.Map;
import java.util.HashMap;
import java.util.StringTokenizer;

import javax.xml.parsers.*;

import org.apache.log4j.Category;
import org.w3c.dom.*;

import com.neoworks.util.XPathQuery;
import com.neoworks.util.DOMUtils;

/**
 * Connection pool manager.
 */
public class PoolManager
{
	private static final Category log = Category.getInstance(PoolManager.class.getName());
	private static final boolean logDebugEnabled = log.isDebugEnabled();
	private static final boolean logInfoEnabled = log.isInfoEnabled();

	/**
	 * The system property we use to locate the properties file.
	 */
	private static final String PROPERTY_NAME = "com.neoworks.connectionpool.properties";
	private static final String RESOURCE_NAME = "databasepool.xml";
	private static final String CONFIGURATION_XPATH = "//databasepool";


	/**
	 * The single instance of this class.
	 */
	private static PoolManager instance = null;


	/**
	 * Statically maintained set of drivers
	 */
	private Set drivers = null;


	/**
	 * Map of all the connection pools that have been created.
	 */
	private Map pools = null;


	/**
	 * Return the static instance of this class.
	 *
	 * @return The single instance of this class, or <code>null</code> if there was a problem reading the properties file, setting up the connection pools and registering drives..
	 */
	static synchronized public PoolManager getInstance()
	{
		if (instance == null)	    // If the singleton instance hasn't been initialised yet
		{
			try
			{
				instance = new PoolManager();
			}
			catch (Exception e)
			{
				instance = null;
				e.printStackTrace();
			}
		}
		return instance;
	}

	/**
	 * Configure the PoolManager from a DOM Element
	 *
	 * @param config The DOM Element to configure from
	 */
	public static synchronized void configure( Element config )
	{
		if (logDebugEnabled) log.debug( "Configuring for element " + DOMUtils.DOM2String( config , true ) );
		instance = new PoolManager( config );
	}

	/**
	 * Configure the PoolManager from a DOM Element, using a specific XPath query
	 *
	 * @param config The DOM Element to configure from
	 * @param xpath The XPath query to use
	 */
	public static synchronized void configure( Element config , String xpath )
	{
		Node targetNode = XPathQuery.getSingleNode( config , xpath );
		if (logDebugEnabled) log.debug( "Configuring for element " + DOMUtils.DOM2String( targetNode , true ) );
		instance = new PoolManager( (Element) XPathQuery.getSingleNode( config , xpath ) );
	}
	
	/**
	 * Private constructor to implement singleton pattern.
	 *
	 * This constructor is called with no argument so it moves into 'guess 
	 * your own configuration' mode so we have to have a look around and 
	 * try to find some configuration for ourselves.
	 */
	private PoolManager() throws IOException, ClassNotFoundException, InstantiationException,IllegalAccessException, SQLException
	{
		init();
		
		ClassLoader cloader = PoolManager.class.getClassLoader();
		
	    // Step 1: Try and find an XML configuration file in the classpath
		if (logDebugEnabled)
		{
			log.debug("Attempting to autoconfigure PoolManager");
			log.debug("Attempting to locate system resource ["+RESOURCE_NAME+"] with the system classloader ["+cloader.getClass().getName()+"]");
		}
		
		URL configResource = cloader.getResource( RESOURCE_NAME );
				
		if ( configResource != null )
	    {
			if (logDebugEnabled) log.debug("Found resource, attempting to parse XML");
			Document dom = null;
			try
			{
				DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
				DocumentBuilder builder = factory.newDocumentBuilder();
				dom = builder.parse( configResource.openStream() );
			}
			catch( Exception e )
			{
				log.error( "An error occurred whilst trying to open an XML resource file for the database pool" , e );
			}

			if ( dom != null )		// If we have a valid DOM parsed from the document
			{
				Element config = null;
				try
				{
					config = (Element) XPathQuery.getSingleNode( dom , CONFIGURATION_XPATH );	    // Recursive search for the configuration node
				}
				catch (Exception e)
				{
					log.error("Oops whilst trying to find configuration node",e);
				}
				if ( config != null )
				{
					loadConfig( config );
					return;
				}
				else
				{
					if (logDebugEnabled) log.debug( "Could not find XPath " + CONFIGURATION_XPATH + " in the document " + DOMUtils.DOM2String( config ) );
				}
			}
		}

		// Step 2: If we've failed to find an XML file check to see if 
		// there is a system parameter as specified in the constants that
		// might help us find a properties file
		
		if (logDebugEnabled) log.debug( "Searching system property "+PoolManager.PROPERTY_NAME+" for properties file" );
		
		String propertiesFile = System.getProperty(PoolManager.PROPERTY_NAME);
		if (propertiesFile == null)
		{
			log.fatal( "System property not specified: " + PROPERTY_NAME );
			log.fatal( "Unable to find any configuration data for database PoolManager.  Exiting." );
			System.exit(1);
		}
		FileInputStream is = new FileInputStream(propertiesFile);
		loadConfig(is);		// Initialise the old way (properties file)
	}

	/**
	 * Private constructor to create and configure this object from a DOM
	 *
	 * @param configElement The DOM Element to use for configuration
	 */
	private PoolManager( Element configElement )
	{
		init();
		loadConfig( configElement );
	}
	
	/**
	 * Initialise any variables that need to be created in any instance
	 */
	private synchronized void init()
	{
		// Set up connection pools.
		pools = new HashMap();
		
		// Set up storage for drivers
		drivers = new HashSet();
	}
		
	/**
	 * Initialise the database pool with the XML node passed, which should
	 * be of the form:
	 *
	 * <pre>
	 * &lt;databasepool&gt;
	 *     &lt;driver classname="org.mydriver.whatever" /&gt;
	 *     ...
	 *
	 *     &lt;pool
	 *     name="poolname"
	 *     url="jdbc url of database"
	 *     user="username to connect to database"
	 *     password=""
	 *     maxconns="maximum connections to reach"
	 *     initconns="initial connections to open"
	 *     logintimeout="timeout for login to database"
	 *     /&gt;
	 *     ...
	 *
	 * &lt;/databasepool&gt;
	 * 
	 * </pre>
	 *
	 * @param configRoot The root Element of the configuration
	 */
	public synchronized void loadConfig( Element configroot )
	{
		if (logDebugEnabled) log.debug( "Configuring with the following XML:\n" + DOMUtils.DOM2String( configroot ) );
		
		// Load in the drivers for the database access
		
		NodeList driverNodes = XPathQuery.getNodes( configroot , "driver" );
		for (int y = driverNodes.getLength() ; --y >= 0 ; )
		{
			Element driverElement = (Element) driverNodes.item( y );
			String driverClassName =  driverElement.getAttribute( "classname" );
			
			if (logDebugEnabled) log.debug( "Instantiating and registering class " + driverClassName );

			try
			{
				Driver driver = (Driver) Class.forName(driverClassName).newInstance();
				DriverManager.registerDriver(driver);
				drivers.add(driver);
			}
			catch ( ClassNotFoundException cnfe )
			{
				log.error( "Could not find class "+driverClassName+" whilst attempting to load" , cnfe );
				continue;
			}
			catch ( InstantiationException inex )
			{
				log.error( "Could not instantiate class "+driverClassName , inex );
				continue;
			}
			catch ( SQLException se )
			{
				log.error( "Could not register class "+driverClassName+" with DriverManager" , se );
				continue;
			}
			catch ( IllegalAccessException iae )
			{
				log.error( "Illegal access exception whilst attempting to load database driver" , iae );
				continue;
			}
		}
		
		// Create all of the pools specified
		
	    NodeList nodes = XPathQuery.getNodes( configroot , "pool" );
		for (int x = nodes.getLength() ; --x >= 0 ; )
	    {
			Element currElement = (Element) nodes.item( x );
			
			if ( currElement.getAttribute( "url" ).length() != 0 )
			{
				String name = currElement.getAttribute("name");
				String url = currElement.getAttribute("url");
				String user = currElement.getAttribute("user");
				String password = currElement.getAttribute("password");

				int maxconns = 0;
				try { maxconns = Integer.parseInt( currElement.getAttribute( "maxconns" ) ); } catch ( NumberFormatException nfe ) { }

				int initconns = 0;
				try { initconns = Integer.parseInt( currElement.getAttribute( "initconns" ) ); } catch ( NumberFormatException nfe ) { }

				int logintimeout = 5;
				try { logintimeout = Integer.parseInt( currElement.getAttribute( "logintimeout" ) ); } catch ( NumberFormatException nfe ) { }

				ConnectionPool pool = new ConnectionPool(name, url, user, password, maxconns, initconns, logintimeout);
				pools.put( name , pool );
			}
			else
			{
				// Skip this as it has no url specified, which is, of course, 
				// the essential part of the configuration data.
				if (logDebugEnabled) log.debug( "No URL specified for the pool specified by "+DOMUtils.DOM2String( currElement ) );
			}
	    }
	}
	
	/**
	 * Configures the instance by reading properties from a given input stream, instantiating drivers and connection
	 * pools.
	 *
	 * <p>Ensure that any resources that are currently allocated are cleaned up, re-read the properties file, register
	 * drivers and set up connection pools.</p>
	 *
	 * @param is The <code>InputStream</code> to load the properties from.
	 * @exception IOException If there was a problem loading the properties from the specified <code>InputStream</code>.
	 * @exception ClassNotFoundException If there was a problem loading one of the driver classes.
	 * @exception InstantiationException If there was a problem instantiating one of the drivers.
	 * @exception IllegalAccessException If there was a problem accessing the class file of one of the drivers.
	 * @exception SQLException If there was a problem registering a driver.
	 */
	public synchronized void loadConfig(InputStream is) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, SQLException
	{	
		if (logDebugEnabled) log.debug("Loading config the horrid old way from the cack old properties file...");
		
		Properties properties = new Properties();
		properties.load(is);

		// Register drivers with the DriverManager.
		String driverClasses = properties.getProperty("drivers");
		StringTokenizer st = new StringTokenizer(driverClasses);
		while (st.hasMoreElements())
		{
			String driverClassName = st.nextToken().trim();
			Driver driver = (Driver) Class.forName(driverClassName).newInstance();
			DriverManager.registerDriver(driver);
			drivers.add(driver);
		}

		Enumeration propNames = properties.propertyNames();
		while (propNames.hasMoreElements())
		{
			String name = (String) propNames.nextElement();
			if (name.endsWith(".url"))
			{
				String poolName = name.substring(0, name.lastIndexOf("."));
				String url = properties.getProperty(poolName + ".url");
				if (url == null)
				{
					continue;
				}

				String user = properties.getProperty(poolName + ".user");
				if (user == null)
				{
					continue;
				}

				String password = properties.getProperty(poolName + ".password");
				if (password == null)
				{
					continue;
				}


				String maxConns = properties.getProperty(poolName + ".maxconns", "0");
				int max;
				try
				{
					max = Integer.valueOf(maxConns).intValue();
				}
				catch (NumberFormatException e)
				{
					continue;
				}

				String initConns = properties.getProperty(poolName + ".initconns", "0");
				int init;
				try
				{
					init = Integer.valueOf(initConns).intValue();
				}
				catch (NumberFormatException e)
				{
					continue;
				}

				String loginTimeOut = properties.getProperty(poolName + ".logintimeout", "5");
				int timeOut;
				try
				{
					timeOut = Integer.valueOf(loginTimeOut).intValue();
				}
				catch (NumberFormatException e)
				{
					continue;
				}

				ConnectionPool pool = new ConnectionPool(poolName, url, user, password, max, init, timeOut);
				pools.put(poolName, pool);
			}
		}
	}


	/**
	 * Clean up resources used by this class.
	 *
	 * @exception SQLException If there was a problem deregistering one of the drivers.
	 */
	public synchronized void release() throws SQLException
	{
		if (pools != null)
		{
			Iterator iterator = pools.values().iterator();
			while (iterator.hasNext())
			{
				ConnectionPool pool = (ConnectionPool) iterator.next();
				iterator.remove();
				pool.release();
			}
		}

		if (drivers != null)
		{
			Iterator iterator = drivers.iterator();
			while (iterator.hasNext())
			{
				Driver driver = (Driver) iterator.next();
				iterator.remove();
				DriverManager.deregisterDriver(driver);
			}
		}
	}

	/**
	 * Return a connection from the pool identified by the given string.
	 *
	 * @param name The string identifier of the connection pool from which we want the connection.
	 * @return A connection from the appropriate pool.
	 */
	public Connection getConnection(String name) throws SQLException
	{
		Connection conn = null;
		ConnectionPool pool = (ConnectionPool) pools.get(name);
		if (pool != null)
		{
			conn = pool.getConnection();
			conn.setAutoCommit(true);
		}
		else
		{
			throw new SQLException("No such pool: " + name);
		}
		return conn;
	}
}
