/*
 * The Apache Software License, Version 1.1
 *
 *
 * Copyright (c) 1999 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Xalan" and "Apache Software Foundation" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache",
 *    nor may "Apache" appear in their name, without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation and was
 * originally based on software copyright (c) 1999, Lotus
 * Development Corporation., http://www.lotus.com.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */
package org.apache.xalan.lib.sql;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.SQLException;

import java.util.Properties;
import java.util.Vector;
import java.util.StringTokenizer;


import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.traversal.NodeIterator;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;

import org.apache.xalan.stree.ElementImpl;

/**
 * An XSLT extension that allows a stylesheet to
 * access JDBC data. From the stylesheet perspective,
 * XConnection provides 3 extension functions: new(),
 * query(), and close().
 * Use new() to call one of XConnection constructors, which
 * establishes a JDBC driver connection to a data source and
 * returns an XConnection object.
 * Then use the XConnection object query() method to return a
 * result set in the form of a row-set element.
 * When you have finished working with the row-set, call the
 * XConnection object close() method to terminate the connection.
 */
public class XConnection
{

  /**
   * Flag for DEBUG mode
   */
  private static final boolean DEBUG = false;


  /**
   * The JDBC connection.
   */
  public Connection m_connection = null;

  /**
   * Reference to the ConnectionPool used
   */
  private ConnectionPool  m_ConnectionPool = null;
  private String          m_ConnectionPoolName;

  private boolean         m_IsDefaultPool = false;
  private boolean         m_DefaultPoolingEnabled = false;


  /**
   * Let's keep a copy of the ConnectionPoolMgr in
   * alive here so we are keeping the static pool alive
   *
   * We will also use this Pool Manager to register our default pools.
   *
   */

   private XConnectionPoolManager m_PoolMgr = new XConnectionPoolManager();

  /**
   * For PreparedStatements, we need a place to
   * to store the parameters in a vector.
   */
   public Vector        m_ParameterList = new Vector();


  // The original constructors will be kept around for backwards
  // compatibility. Future Stylesheets should use the approaite
  // connect method to receive full error information.
  //
  public XConnection (String ConnPoolName)
  {
    connect(ConnPoolName);
  }

  public XConnection(String driver, String dbURL)
  {
    connect(driver, dbURL);
  }

  public XConnection(NodeList list)
  {
    connect(list);
  }

  public XConnection(String driver, String dbURL, String user,
                     String password)
  {
    connect(driver, dbURL, user, password);
  }

  public XConnection(String driver, String dbURL, Element protocolElem)
  {
    connect(driver, dbURL, protocolElem);
  }

  /**
   *
   * Create an XConnection using the name of an existing Connection Pool
   * @param <code>String poolName</code>, name of the existing pool
   * to pull connections from.
   *
   */
  public NodeIterator connect(String ConnPoolName)
  {
    try
    {
      if ((m_ConnectionPool != null) && (m_IsDefaultPool))
        m_PoolMgr.removePool(m_ConnectionPoolName);

      m_ConnectionPool = m_PoolMgr.getPool(ConnPoolName);
      m_ConnectionPoolName = ConnPoolName;

      m_connection = m_ConnectionPool.getConnection();
    }
    catch(SQLException e)
    {
      SQLExtensionError err = new SQLExtensionError(e);
      return err;
    }

    return null;
  }

  /**
   * Create an XConnection object with just a driver and database URL.
   * @param driver JDBC driver of the form foo.bar.Driver.
   * @param dbURL database URL of the form jdbc:subprotocol:subname.
   */

  public NodeIterator connect(String driver, String dbURL)
  {
    try
    {
      init(driver, dbURL, new Properties() );
    }
    catch(SQLException e)
    {
      SQLExtensionError err = new SQLExtensionError(e);
      return err;
    }
    catch (Exception e)
    {
      ExtensionError err = new ExtensionError(e);
      return err;
    }

    return null;

  }

  public NodeIterator connect(Element protocolElem)
  {
    try
    {
      initFromElement(protocolElem);
    }
    catch(SQLException e)
    {
      SQLExtensionError err = new SQLExtensionError(e);
      return err;
    }
    catch (Exception e)
    {
      ExtensionError err = new ExtensionError(e);
      return err;
    }

    return null;

  }

  public NodeIterator connect(NodeList list)
  {
    try
    {
      initFromElement( (Element) list.item(0) );
    }
    catch(SQLException e)
    {
      SQLExtensionError err = new SQLExtensionError(e);
      return err;
    }
    catch (Exception e)
    {
      ExtensionError err = new ExtensionError(e);
      return err;
    }

    return null;
  }

  /**
   * Create an XConnection object with user ID and password.
   * @param driver JDBC driver of the form foo.bar.Driver.
   * @param dbURL database URL of the form jdbc:subprotocol:subname.
   * @param user user ID.
   * @param password connection password.
   */
  public NodeIterator connect(String driver, String dbURL, String user,
                     String password)
  {
    try
    {
      Properties prop = new Properties();
      prop.put("user", user);
      prop.put("password", password);

      init(driver, dbURL, prop);
    }
    catch(SQLException e)
    {
      SQLExtensionError err = new SQLExtensionError(e);
      return err;
    }
    catch (Exception e)
    {
      ExtensionError err = new ExtensionError(e);
      return err;
    }

    return null;
  }


  /**
   * Create an XConnection object with a connection protocol
   * @param driver JDBC driver of the form foo.bar.Driver.
   * @param dbURL database URL of the form jdbc:subprotocol:subname.
   * @param protocolElem list of string tag/value connection arguments,
   * normally including at least "user" and "password".
   */
  public NodeIterator connect(String driver, String dbURL, Element protocolElem)
  {
    try
    {
      Properties prop = new Properties();

      NamedNodeMap atts = protocolElem.getAttributes();

      for (int i = 0; i < atts.getLength(); i++)
      {
        prop.put(atts.item(i).getNodeName(), atts.item(i).getNodeValue());
      }

      init(driver, dbURL, prop);
    }
    catch(SQLException e)
    {
      SQLExtensionError err = new SQLExtensionError(e);
      return err;
    }
    catch (Exception e)
    {
      ExtensionError err = new ExtensionError(e);
      return err;
    }

    return null;
  }


  /**
   *
   * Allow the database connection information to be sepcified in
   * the XML tree. The connection information could also be
   * externally originated and passed in as an XSL Parameter.
   *
   * The required XML Format is as follows.
   *
   * // A document fragment is needed to specify the connection information
   * // the top tag name is not specific for this code, we are only interested
   * // in the tags inside.
   * <DBINFO-TAG>
   *
   * // Specify the driver name for this connection pool
   *  <dbdriver>drivername</dbdriver>
   *
   * // Specify the URL for the driver in this connection pool
   *  <dburl>url</dburl>
   *
   * // Specify the password for this connection pool
   *  <password>password</password>
   *
   * // Specify the username for this connection pool
   *  <user>username</user>
   *
   *  // You can add extra protocol items including the User Name & Password
   *  // with the protocol tag. For each extra protocol item, add a new element
   *  // where the name of the item is specified as the name attribute and
   *  // and its value as the elements value.
   *  <protocol name="name of value">value</protocol>
   *
   * </DBINFO-TAG>
   *
   */
  private void initFromElement(Element e)
    throws SQLException
  {

    Properties prop = new Properties();
    String driver = "";
    String dbURL = "";
    Node n = e.getFirstChild();

    if (null == n) return; // really need to throw an error

    do
    {
      String nName = n.getNodeName();

      if (nName.equalsIgnoreCase("dbdriver"))
      {
        driver = "";
        Node n1 = n.getFirstChild();
        if (null != n1)
        {
          driver = n1.getNodeValue();
        }
      }

      if (nName.equalsIgnoreCase("dburl"))
      {
        dbURL = "";
        Node n1 = n.getFirstChild();
        if (null != n1)
        {
          dbURL = n1.getNodeValue();
        }
      }

      if (nName.equalsIgnoreCase("password"))
      {
        String s = "";
        Node n1 = n.getFirstChild();
        if (null != n1)
        {
          s = n1.getNodeValue();
        }
        prop.put("password", s);
      }

      if (nName.equalsIgnoreCase("user"))
      {
        String s = "";
        Node n1 = n.getFirstChild();
        if (null != n1)
        {
          s = n1.getNodeValue();
        }
        prop.put("user", s);
      }

      if (nName.equalsIgnoreCase("protocol"))
      {
        String Name = "";

        NamedNodeMap attrs = n.getAttributes();
        Node n1 = attrs.getNamedItem("name");
        if (null != n1)
        {
          String s = "";
          Name = n1.getNodeValue();

          Node n2 = n.getFirstChild();
          if (null != n2) s = n2.getNodeValue();

          prop.put(Name, s);
        }
      }

    } while ( (n = n.getNextSibling()) != null);

    init(driver, dbURL, prop);
  }



  /**
   * Initilize is being called because we did not have an
   * existing Connection Pool, so let's see if we created one
   * already or lets create one ourselves.
   *
   * @param driver JDBC driver of the form foo.bar.Driver.
   * @param dbURL database URL of the form jdbc:subprotocol:subname.
   * @param Properties list of string tag/value connection arguments,
   * normally including at least "user" and "password".
   * @param getConnectionArgs Connection arguments
   */
  private void init(String driver, String dbURL, Properties prop)
    throws SQLException
  {
    if (DEBUG)
      System.out.println("XConnection, Connection Init");

    String user = prop.getProperty("user");
    if (user == null) user = "";

    String passwd = prop.getProperty("password");
    if (passwd == null) passwd = "";


    String poolName = driver + dbURL + user + passwd;
    ConnectionPool cpool = m_PoolMgr.getPool(poolName);

    // We are limited to only one Default Connection Pool
    // at a time.
    // If we are creating a new Default Pool, release the first
    // One so it is not lost when we close the Stylesheet
    if (
        (m_ConnectionPool != null) &&
        (m_IsDefaultPool) &&
        (cpool != m_ConnectionPool))
    {
      m_PoolMgr.removePool(m_ConnectionPoolName);
    }

    if (cpool == null)
    {

      if (DEBUG)
      {
        System.out.println("XConnection, Creating Connection");
        System.out.println(" Driver  :" + driver);
        System.out.println(" URL     :" + dbURL);
        System.out.println(" user    :" + user);
        System.out.println(" passwd  :" + passwd);
      }


      DefaultConnectionPool defpool = new DefaultConnectionPool();

      if ((DEBUG) && (defpool == null))
        System.out.println("Failed to Create a Default Connection Pool");

      defpool.setDriver(driver);
      defpool.setURL(dbURL);
      defpool.setProtocol(prop);


      // Only enable pooling in the default pool if we are explicatly
      // told too.
      if (m_DefaultPoolingEnabled) defpool.enablePool();

      m_PoolMgr.registerPool(poolName, defpool);

      m_ConnectionPool = defpool;
      m_ConnectionPoolName = poolName;
    }
    else
    {
      if (DEBUG)
        System.out.println("Default Connection already existed");

      m_ConnectionPool = cpool;
      m_ConnectionPoolName = poolName;
    }

    m_ConnectionPool.testConnection();

    m_connection = m_ConnectionPool.getConnection();
  }


  /**
   * Execute a query statement by instantiating an
   * {@link org.apache.xalan.lib.sql.XStatement XStatement}
   * object. The XStatement executes the query, and uses the result set
   * to create a {@link org.apache.xalan.lib.sql.RowSet RowSet},
   * a row-set element.
   *
   * @param queryString the SQL query.
   * @return XStatement implements NodeIterator.
   *
   * @throws SQLException
   */
  public NodeIterator query(String queryString)
  {
    try
    {
      return new XStatement(this, queryString);
    }
    catch(SQLException e)
    {
      if (DEBUG)
      {
        System.out.println("SQL Exception in Query");
        e.printStackTrace();
      }

      SQLExtensionError err = new SQLExtensionError(e);
      return err;
    }
    catch (Exception e)
    {
      if (DEBUG)
      {
        System.out.println("Exception in Query");
        e.printStackTrace();
      }

      ExtensionError err = new ExtensionError(e);
      return err;
    }

  }

  /**
   * Execute a parameterized query statement by instantiating an
   * {@link org.apache.xalan.lib.sql.XStatement XStatement}
   * object. The XStatement executes the query, and uses the result set
   * to create a {@link org.apache.xalan.lib.sql.RowSet RowSet},
   * a row-set element.
   *
   * @param queryString the SQL query.
   * @return XStatement implements NodeIterator.
   *
   * @throws SQLException
   */
  public NodeIterator pquery(String queryString)
  {
    try
    {
      return new XStatement(this, queryString, m_ParameterList);
    }
    catch(SQLException e)
    {
      SQLExtensionError err = new SQLExtensionError(e);
      return err;
    }
    catch (Exception e)
    {
      ExtensionError err = new ExtensionError(e);
      return err;
    }


  }


  /**
   * Execute a parameterized query statement by instantiating an
   * {@link org.apache.xalan.lib.sql.XStatement XStatement}
   * object. The XStatement executes the query, and uses the result set
   * to create a {@link org.apache.xalan.lib.sql.RowSet RowSet},
   * a row-set element.
   * This method allows for the user to pass in a comma seperated
   * String that represents a list of parameter types. If supplied
   * the parameter types will be used to overload the current types
   * in the current parameter list.
   *
   * @param queryString the SQL query.
   * @return XStatement implements NodeIterator.
   *
   * @throws SQLException
   */
  public NodeIterator pquery(String queryString, String typeInfo)
  {
    try
    {
      int indx = 0;
      QueryParameter param = null;

      // Parse up the parameter types that were defined
      // with the query
      StringTokenizer plist = new StringTokenizer(typeInfo);

      // Override the existing type that is stored in the
      // parameter list. If there are more types than parameters
      // ignore for now, a more meaningfull error should occur
      // when the actual query is executed.
      while (plist.hasMoreTokens())
      {
        String value = plist.nextToken();
        param = (QueryParameter) m_ParameterList.elementAt(indx);
        if ( null != param )
        {
          param.setType(value);
        }
      }

      return new XStatement(this, queryString, m_ParameterList);
    }
    catch(SQLException e)
    {
      SQLExtensionError err = new SQLExtensionError(e);
      return err;
    }
    catch (Exception e)
    {
      ExtensionError err = new ExtensionError(e);
      return err;
    }


  }

  /**
   * Add an untyped value to the parameter list.
   */
  public void addParameter(String value)
  {
    addParameterWithType(value, null);
  }

  /**
   * Add a typed parameter to the parameter list.
   */
  public void addParameterWithType(String value, String Type)
  {
    m_ParameterList.addElement( new QueryParameter(value, Type) );
  }


  /**
   * Add a single parameter to the parameter list
   * formatted as an Element
   */
  public void addParameterFromElement(Element e)
  {
    NamedNodeMap attrs = e.getAttributes();
    Node Type = attrs.getNamedItem("type");
    Node n1  = e.getFirstChild();
    if (null != n1)
    {
      String value = n1.getNodeValue();
      if (value == null) value = "";
      m_ParameterList.addElement( new QueryParameter(value, Type.getNodeValue()) );
    }
  }


  /**
   * Add a section of parameters to the Parameter List
   * Do each element from the list
   */
  public void addParameterFromElement(NodeList nl)
  {
    //
    // Each child of the NodeList represents a node
    // match from the select= statment. Process each
    // of them as a seperate list.
    // The XML Format is as follows
    //
    // <START-TAG>
    //   <TAG1 type="int">value</TAG1>
    //   <TAGA type="int">value</TAGA>
    //   <TAG2 type="string">value</TAG2>
    // </START-TAG>
    //
    // The XSL to process this is formatted as follows
    // <xsl:param name="plist" select="//START-TAG" />
    // <sql:addParameter( $plist );
    //
    int count = nl.getLength();
    for (int x=0; x<count; x++)
    {
      addParameters( (Element) nl.item(x));
    }
  }

  private void addParameters(Element elem)
  {
    //
    // Process all of the Child Elements
    // The format is as follows
    //
    //<TAG type ="typeid">value</TAG>
    //<TAG1 type ="typeid">value</TAG1>
    //<TAGA type ="typeid">value</TAGA>
    //
    // The name of the Node is not important just is value
    // and if it contains a type attribute

    Node n = elem.getFirstChild();

    if (null == n) return;

    do
    {
      if (n.getNodeType() == Node.ELEMENT_NODE)
      {
        NamedNodeMap attrs = n.getAttributes();
        Node Type = attrs.getNamedItem("type");
        String TypeStr;

        if (Type == null) TypeStr = "string";
        else TypeStr = Type.getNodeValue();

        Node n1  = n.getFirstChild();
        if (null != n1)
        {
          String value = n1.getNodeValue();
          if (value == null) value = "";


          m_ParameterList.addElement(
            new QueryParameter(value, TypeStr) );
        }
      }
    } while ( (n = n.getNextSibling()) != null);
  }


  /**
   *
   * There is a problem with some JDBC drivers when a Connection
   * is open and the JVM shutsdown. If there is a problem, there
   * is no way to control the currently open connections in the
   * pool. So for the default connection pool, the actuall pooling
   * mechinsm is disabled by default. The Stylesheet designer can
   * re-enabled pooling to take advantage of connection pools.
   * The connection pool can even be disabled which will close all
   * outstanding connections.
   *
   *
   */
  public void enableDefaultConnectionPool()
  {

    if (DEBUG)
      System.out.println("Enabling Default Connection Pool");

    m_DefaultPoolingEnabled = true;

    if (m_ConnectionPool == null) return;
    if (!m_IsDefaultPool) return;

    m_ConnectionPool.enablePool();

  }

  /**
   * See enableDefaultConnectionPool
   */
  public void disableDefaultConnectionPool()
  {
    if (DEBUG)
      System.out.println("Disabling Default Connection Pool");

    m_DefaultPoolingEnabled = false;

    if (m_ConnectionPool == null) return;
    if (!m_IsDefaultPool) return;

    m_ConnectionPool.disablePool();
  }

  /**
   * Close the connection to the data source.
   *
   *
   * @throws SQLException
   */
  public void close() throws SQLException
  {

    if (DEBUG)
      System.out.println("Entering XConnection.close");

    if (null != m_connection)
    {
      if (null != m_ConnectionPool)
      {
        m_ConnectionPool.releaseConnection(m_connection);

      }
      else
      {
        // something is wrong here, we have a connection
        // but no controlling pool, close it anyway the
        // error will show up as an excpeion elsewhere
        m_connection.close();
      }
    }

    m_connection = null;

    if (DEBUG)
      System.out.println("Exiting XConnection.close");
  }

  protected void finalize()
  {
    if (DEBUG) System.out.println("In XConnection, finalize");
  }
}
