/*
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999 Bull S.A.
 * Contact: jonas-team@objectweb.org
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * Initial developer(s): ____________________________________.
 * Contributor(s): ______________________________________.
 *
 * 00/01/08 Markus Fritz
 *          make connection pooling thread safe (with same tx)
 *          introduce MaxAge for connections
 *          optimize pooling
 *          add some 'paranoia code': test conns before returning
 *	    them from the pool.
 * 00/18/04 Jun Inamori (j-office@osa.att.ne.jp)
 *          For closing correctly all connections at server shutdown.
 * 01/11/06 Christophe Ney cney@batisseurs.com for Lutris Technologies
 *          Added ResourceManagerListener mechanism.
 *
 * 02/01/15 Dean Jennings - Map   instead of Hashtable  for cmList
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */


package org.objectweb.jonas.dbm;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import java.util.HashMap;
import java.util.Collections;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.sql.ConnectionEvent;
import javax.sql.ConnectionEventListener;
import javax.sql.DataSource;
import javax.sql.XAConnection;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.xa.XAResource;
import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.jdbc_xa.XADataSourceImpl;
import org.objectweb.jonas.jtm.TransactionService;
import org.objectweb.jonas.service.ServiceManager;
import org.objectweb.jonas.service.ServiceException;
import org.objectweb.transaction.api.ResourceManagerEvent;
import org.objectweb.transaction.api.ResourceManagerEventListener;
import org.objectweb.util.monolog.api.Logger;
import org.objectweb.util.monolog.api.BasicLevel;

/**
 * JOnAS generic implementation of DataSource to manage an underlying
 * JDBC-XA driver (XADataSource, XAConnection, XAResource, ...)
 * This class acts as a factory for Connection objects.
 * Its main goal is to manage a pool of XAConnection objects, to avoid 
 * closing the actual Connection on DBMS, and to deal with distributed
 * transactions.
 * This DataSource is registered in JNDI (=> implements Referenceable)
 *
 * @author Philippe Durieux
 * 
 * Contributor(s): 
 *
 * 00/01/08 Markus Fritz
 *          make connection pooling thread safe (with same tx)
 *          introduce MaxAge for connections
 *          optimize pooling
 *          add some 'paranoia code': test conns before returning
 *	    them from the pool.
 * 00/18/04 Jun Inamori (j-office@osa.att.ne.jp)
 *          For closing correctly all connections at server shutdown.
 * 01/11/06 Christophe Ney cney@batisseurs.com for Lutris Technologies
 *          Added ResourceManagerListener mechanism.
 *
 * 02/01/15 Dean Jennings - Map   instead of Hashtable  for cmList
 *
 */

public class ConnectionManager implements DataSource, 
					  Referenceable, 
					  Serializable,
                                          ConnectionEventListener,
                                          ConnectionManagerMBean {

    static private Logger logger = null;

    // The static table of all the ConnectionManager objects
    // Each ResourceManager has its own ConnectionManager (i.e. DataSource)
    private static Map cmList = new HashMap();

    /**
     * TransactionManager object
     * @serial
     */
    private TransactionManager tm = null;

    /**
     * underlaying XA DataSource
     * @serial
     */
    private XADataSourceImpl xads = null;

    /**
     * The pool of XAConnections managed by this Connection Manager
     * @serial
     */
    private Pool pool = null;

    // ----------------------------------------------------------
    // properties
    // ----------------------------------------------------------

    /**
     * @serial for JNDI
     */
    private String dataSourceName = null;
    /**
     * @serial url for database
     */
    private String url = null;
    /**
     * @serial JDBC driver Class
     */
    private String className = null;
    /**
     * @serial default user
     */
    private String userName = null;
    /**
     * @serial default passwd
     */
    private String password = null;
    /**
     * @serial 
     */
    private String connchecklevel = null;
    /**
     * @serial 
     */
    private String connmaxage = null;
    /**
     * @serial 
     */
    private String connteststmt = null;
    /**
     * @serial 
     */
    private int isolationLevel = -1;
    private String isolationStr = null;
    /**
     * MBean variable
     */
    private String xaName = null;
    private String dsDescription = null;

    private static ResourceManagerEventListener rmListener = null;
    
    // -----------------------------------------------------------------
    // Constructors
    // -----------------------------------------------------------------

    /**
     * Constructor for Factory
     */
    public ConnectionManager() {
        logger = Log.getLogger(Log.JONAS_DBM_PREFIX);
	// Set the TransactionManager
	TransactionService ts =
	    (TransactionService) ServiceManager.getInstance().getTransactionService();
	tm = ts.getTransactionManager();

	// set threadData as being the listener for this RM
	setRMEListener((ResourceManagerEventListener) tm);       

	pool = new Pool(this);

        // Create the underlaying JDBC-XA driver
        // We do not use JNDI here. (should we do ?)
        xads = new XADataSourceImpl();
    }

    // -----------------------------------------------------------------
    // Properties
    // -----------------------------------------------------------------

    public void setRMEListener(ResourceManagerEventListener rmel) {
	rmListener = rmel;
    }

    public String getDataSourceName() {
        return dataSourceName;
    }
    public void setDataSourceName(String s) {
        dataSourceName = s;
        xads.setDataSourceName(s); // for getRMID!

        // Add it to the list
        cmList.put(s, this);
    }

    public String getUrl() {
        return url;
    }
    public void setUrl(String s) {
        url = s;
        // Forward it to JDBC-XA driver
        xads.setUrl(s);
    }

    public String getClassName() {
        return className;
    }
    public void setClassName(String s) {
        className = s;
        // Forward it to JDBC-XA driver
        xads.setClassName(s);
    }

    public String getUserName() {
        return userName;
    }
    public void setUserName(String s) {
        userName = s;
        // Forward it to JDBC-XA driver
        xads.setUserName(s);
    }

    public String getPassword() {
        return password;
    }
    public void setPassword(String s) {
        password = s;
        // Forward it to JDBC-XA driver
        xads.setPassword(s);
    }

    public void setTransactionIsolation(String level) {
	if (level.equals("serializable")) {
	    isolationLevel = Connection.TRANSACTION_SERIALIZABLE;
	} else if (level.equals("none")) {
	    isolationLevel = Connection.TRANSACTION_NONE;
	} else if (level.equals("read_committed")) {
	    isolationLevel = Connection.TRANSACTION_READ_COMMITTED;
	} else if (level.equals("read_uncommitted")) {
	    isolationLevel = Connection.TRANSACTION_READ_UNCOMMITTED;
	} else if (level.equals("repeatable_read")) {
	    isolationLevel = Connection.TRANSACTION_REPEATABLE_READ;
	} else {
	    isolationStr = "default";
	    return;
	}
        // Forward it to JDBC-XA driver
	isolationStr = level;
        xads.setTransactionIsolation(isolationLevel);
    }

    public String getTransactionIsolation() {
	return isolationStr;
    }

    /**
     * Configure the Connection pool. Called by the Container at init.
     * Configuration can be set in datasource.properties files.
     */
    public void poolConfigure(String connchecklevel,
			      String connmaxage,
			      String connteststmt) {

	// Keep String values for getReference
	this.connchecklevel = connchecklevel;
	this.connmaxage = connmaxage;
	this.connteststmt = connteststmt;

	// Configure pool
	pool.jdbcConnCheckLevel = (new Integer(connchecklevel)).intValue();
	pool.jdbcConnMaxAge = (new Integer(connmaxage)).intValue();
	pool.jdbcTestStmt = connteststmt;
	logger.log(BasicLevel.DEBUG, "ConnectionManager configured with:");
	logger.log(BasicLevel.DEBUG, "   jdbcConnCheckLevel  = "+pool.jdbcConnCheckLevel);
	logger.log(BasicLevel.DEBUG, "   jdbcConnMaxAge      = "+pool.jdbcConnMaxAge);
	logger.log(BasicLevel.DEBUG, "   jdbcTestStmt        = "+pool.jdbcTestStmt);
	// set times in milliseconds
	pool.jdbcConnMaxAge = pool.jdbcConnMaxAge * 60L * 1000L;
    }

    // -----------------------------------------------------------------
    // DataSource implementation
    // -----------------------------------------------------------------

    /**
     * Attempt to establish a database connection.
     *
     * @return a Connection to the database
     *
     * @exception java.sql.SQLException - if a database-access error occurs.
     */
    public Connection getConnection() throws SQLException {
        return getConnection(userName, password);
    }

    /**
     * Attempt to establish a database connection.
     *
     * @param user - the database user on whose behalf the Connection is being made
     * @param password - the user's password
     *
     * @return a Connection to the database
     *
     * @exception SQLException - if a database-access error occurs.
     */
    public Connection getConnection(String username, String password) throws SQLException {
        XAConnection xac = null;
        Transaction tx = null;
        try {
            tx = tm.getTransaction();
	} catch (NullPointerException n) {
	    // current is null: we are not in EJBServer.
	    logger.log(BasicLevel.ERROR, "ConnectionManager: should not be used outside an EJBServer");
        } catch (SystemException e) {
            logger.log(BasicLevel.ERROR, "ConnectionManager: getTransaction failed:"+e);
        }
	logger.log(BasicLevel.DEBUG, "Tx = " + tx);

        // Check if available XAConnection in the pool for this user

        PoolItem pi = pool.openConnection(username, (Transaction) tx);
        if (pi == null) {
            // Get a new XAConnection
            xac = xads.getXAConnection(username, password);
            // Put it in the pool as in-use

	    pi = pool.newConnection(xac, username, (Transaction) tx);
            // Register me as a ConnectionEventListener
            xac.addConnectionEventListener(this);
        } else {
            // We found a reusable XAConnection
            // We are still registered as ConnectionEventListener
            xac = pi.xaConn;
        }

	Connection ret = xac.getConnection();
	// Enlist XAResource if we are actually in a transaction
	if (tx != null) {
	    if (pi.open == 1) { // Only if first/only thread
		try {
		    tx.enlistResource(xac.getXAResource());
		} catch (RollbackException e) {
		    // Although tx has been marked to be rolled back, 
		    // XAResource has been correctly enlisted.
		    logger.log(BasicLevel.WARN, "XAResource enlisted, but tx is marked rollback");
		} catch (Exception e) {
		    logger.log(BasicLevel.ERROR, "Cannot enlist XAResource:"+e);
		    logger.log(BasicLevel.ERROR, "Connection will not be enlisted in a transaction");
		    // should return connection in the pool XXX
		    throw new SQLException("Cannot enlist XAResource");
		}
		// We are inside a transaction: no autocommit!
		ret.setAutoCommit(false);
	    }
	} else {
	    // We are not in a transaction yet: we just make a pre-registration.
	    ret.setAutoCommit(true);    // in case we do not start a Tx
	    rmListener.connectionOpened(pi);
	}
	
	// return a Connection object
	return ret;
    }

    /**
     * Get the log writer for this data source. 
     * 
     * The log writer is a character output stream to which all logging and 
     * tracing messages for this data source object instance will be printed. 
     * This includes messages printed by the methods of this object, messages 
     * printed by methods of other objects manufactured by this object, and so on.
     * Messages printed to a data source specificlog writer are not printed to the 
     * log writer associated with the Drivermanager class. When a data
     * source object is created the log writer is initially null, in other words, 
     * logging is disabled.
     *
     * @return the log writer for this data source, null if disabled
     *
     * @exception SQLException - if a database-access error occurs.
     */
    public java.io.PrintWriter getLogWriter() throws SQLException {
        return xads.getLogWriter();
    }


    /**
     * Set the log writer for this data source. 
     * 
     * The log writer is a character output stream to which all logging and 
     * tracing messages for this data source object instance will be printed. 
     * This includes messages printed by the methods of this object, messages printed
     * by methods of other objects manufactured by this object, and so on. 
     * Messages printed to a data source specific log writer are not printed to 
     * the log writer associated with the Drivermanager class. When a data
     * source object is created the log writer is initially null, in other words, 
     * logging is disabled.
     * 
     * @param out - the new log writer; to disable, set to null
     *
     * @exception SQLException - if a database-access error occurs.
     */
    public void setLogWriter(java.io.PrintWriter out) throws SQLException {
        xads.setLogWriter(out);
    }


    /**
     * Sets the maximum time in seconds that this data source will wait while 
     * attempting to connect to a database.
     * A value of zero specifies that the timeout is the default system timeout 
     * if there is one; 
     * otherwise it specifies that there is no timeout. 
     * When a data source object is created the login timeout is initially zero.
     *
     * @param seconds - the data source login time limit
     *
     * @exception SQLException - if a database access error occurs.
     */
    public void setLoginTimeout(int seconds) throws SQLException {
        xads.setLoginTimeout(seconds);
    }

    /**
     * Gets the maximum time in seconds that this data source can wait while attempting 
     * to connect to a database.
     * A value of zero means that the timeout is the default system timeout if there is one; 
     * otherwise it means that there is no timeout. When a data source object is created 
     * the login timeout is initially zero.
     *
     * @return the data source login time limit
     *
     * @exception SQLException - if a database access error occurs.
     */
    public int getLoginTimeout() throws SQLException {
        return xads.getLoginTimeout();
    }

    // -----------------------------------------------------------------
    // ConnectionEventListener implementation
    // -----------------------------------------------------------------

    /**
     * Invoked when the application calls close() on its representation 
     * of the connection.
     * @param event - an event object describing the source of the event
     */
    public void connectionClosed(ConnectionEvent event) {
        logger.log(BasicLevel.DEBUG, " ");

        XAConnection xac = (XAConnection) event.getSource();
	PoolItem pi = pool.closeConnection(xac, XAResource.TMSUCCESS);
	
	// remove it from the list of open connections for this thread
	// only if it was opened outside a tx.
	if (pi != null && pi.tx == null) {
	    rmListener.connectionClosed(pi);
	}
    }

    /**
     * Invoked when a fatal connection error occurs, just before an
     * SQLException is thrown to the application.
     * @param event - an event object describing the source of the event
     */
    public void connectionErrorOccurred(ConnectionEvent event) {
        logger.log(BasicLevel.DEBUG, "entering ");

        XAConnection xac = (XAConnection) event.getSource();
	PoolItem pi = pool.closeConnection(xac, XAResource.TMFAIL);

	// remove it from the list of open connections for this thread
	// only if it was opened outside a tx.
	if (pi != null && pi.tx == null) {
	    rmListener.connectionErrorOccured(pi);
	}
    }

    // -----------------------------------------------------------------
    // Referenceable implementation
    // -----------------------------------------------------------------

    /**
     * Retrieves the Reference of this object. Used at binding time by JNDI
     * to build a reference on this object.
     * 
     * @return  The non-null Reference of this object. 
     * @exception  NamingException  If a naming exception was encountered while 
     * retrieving the reference.  
     */
    public Reference getReference() throws NamingException {

        Reference ref = new Reference (this.getClass().getName(), 
                                       "org.objectweb.jonas.dbm.DataSourceFactory",
                                       null);
        // These values are used by ObjectFactory (see DataSourceFactory.java)
        ref.add(new StringRefAddr("datasource.name", getDataSourceName()));
        ref.add(new StringRefAddr("datasource.url", getUrl()));
        ref.add(new StringRefAddr("datasource.classname", getClassName()));
        ref.add(new StringRefAddr("datasource.username", getUserName()));
        ref.add(new StringRefAddr("datasource.password", getPassword()));
	ref.add(new StringRefAddr("datasource.isolationlevel", getTransactionIsolation()));
	ref.add(new StringRefAddr("connchecklevel", connchecklevel));
	ref.add(new StringRefAddr("connmaxage", connmaxage));
	ref.add(new StringRefAddr("connteststmt", connteststmt));
        return ref;
    }

   /**
    * get the ConnectionManager matching the DataSource name
    */
   public static ConnectionManager getConnectionManager(String dsname) {
       
       ConnectionManager cm = (ConnectionManager) cmList.get(dsname);
       return cm;
   }

   public void closeAllConnection(){
	logger.log(BasicLevel.DEBUG, "entering ");
	pool.closeAllConnections();
    }

    public static ResourceManagerEventListener getResourceManagerEventListener() {
        return rmListener;
    }

    /**
     * MBean method:
     * @return Integer number of open connection in this ds
     */
    public Integer getCurrentNumberOfJDBCConnectionOpen() {
	return new Integer(pool.getCurrentNumberOfXAConnection());
    }

    /**
     * MBean method:
     * @return datasource name
     */
    public String getDSName() {
	return dataSourceName;
    }

    /**
     * MBean method:
     * @return the data source factory
     * none or JonasStandardXADataSource
     */
    public String getDataSourceFactory() {
	return "none";
    }

    /**
     * Management method:
     * @param the DataSourceXAName
     */
    public void setDataSourceXADataName(String xaName) {
	this.xaName = xaName;
    } 
    /**
     * MBean method:
     * @return the DataSourceXAName
     */
    public String getDataSourceXADataName() {
	return xaName;
    }
 
    /**
     * MBean method:
     * @return connection level
     */
    public Integer getJDBCConnCheckLevel() {
	return new Integer(connchecklevel);
    }

    /**
     * MBean method:
     * @param connection level
     */
    public void setJDBCConnCheckLevel(Integer level) {
	connchecklevel = level.toString();
	poolConfigure(connchecklevel, connmaxage, connteststmt);
    }


    /**
     * MBean method:
     * @return connection max age
     */
    public Integer getJDBCConnMaxAge(){
	return new Integer(connmaxage);
    }
    /**
     * MBean method:
     * @param connection max age
     */
    public void setJDBCConnMaxAge(Integer age) {
	connmaxage = age.toString();
	poolConfigure(connchecklevel, connmaxage, connteststmt);
    }

    /**
     * MBean method:
     * @return test statement 
     */
    public String getJDBCTestStatement(){
	return connteststmt;
    }
    /**
     * MBean method:
     * @param test statement 
     */
    public void setJDBCTestStatement(String test) {
	connteststmt = test;
	poolConfigure(connchecklevel, connmaxage, connteststmt);
    }
    
    /**
     * Mbean Method:
     * return the type of this data source
     */
    public String getDataSourceType() {
	String result = null;
	if (getClassName().equals("oracle.jdbc.driver.OracleDriver")) result = "Oracle";
	if (getClassName().equals("interbase.interclient.Driver")) result = "InterBase";
	if (getClassName().equals("org.enhydra.instantdb.jdbc.idbDriver")) result = "Instant DB";
	if (getClassName().equals("postgresql.Driver")) result = "Postgres";     // For V. 6.x
	if (getClassName().equals("org.postgresql.Driver")) result = "Postgres"; // For V. 7.x
	return result;
    }

    /**
     * MBean Method:
     * @return the desrciption of this datasource
     */
    public String getDataSourceDescription() {
	return dsDescription;
    }

    /**
     * Management Method:
     * @param the desrciption of this datasource
     */
    public void setDataSourceDescription(String dsDesc) {
	dsDescription = dsDesc;
    }


    // MBean method not avaible in this type of connection
    /**
     * MBean method:
     * @return null
     */
    public Integer getJDBCMinConnPool(){
	return null;
    }
   /**
     * MBean method: do nothing 
     * @param 
     */
    public void setJDBCMinConnPool(Integer min){
	// do nothing for this Connection type
    }
    /**
     * MBean method:
     * @return null
     */
    public Integer getJDBCMaxConnPool(){
	return null;
    }
   /**
     * MBean method: do nothing 
     * @param 
     */
    public void setJDBCMaxConnPool(Integer max){
	// do nothing for this Connection type
    }
    /**
     * MBean method:
     * @return null
     */
    public Integer getJDBCSleepTimeOut(){
	return null;
    }
   /**
     * MBean method: do nothing 
     * @param 
     */
    public void setJDBCSleepTimeOut(Integer timeOut){
	// do nothing for this Connection type
    }
    /**
     * MBean method:
     * @return null
     */
    public Integer getJDBCDeadLockPool(){
	return null;
    }
   /**
     * MBean method: do nothing 
     * @param 
     */
    public void setJDBCDeadLockPool(Integer size){
	// do nothing for this Connection type
    }
    /**
     * MBean method:
     * @return null
     */
    public Integer getJDBCLoopWaitPool(){
	return null;
    }
   /**
     * MBean method: do nothing 
     * @param 
     */
    public void setJDBCLoopWaitPool(Integer size){
	// do nothing for this Connection type
    }







}
