/*-----------------------------------------------------------------------------
 *
 * Enhydra JdbcS
 * The Initial Developer of the Original Code is Lutris Technologies Inc.
 * Portions created by Lutris are Copyright 1997-2000 Lutris Technologies Inc.
 * All Rights Reserved.
 *
 * The contents of this file are subject to the Enhydra 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
 * www.enhydra.org/license/epl.html
 *
 * Software distributed under the License is distributed on an "ASIS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
 * the specific language governing rights and limitations under the License.
 *
 * -----------------------------------------------------------------------------
 * $Id$
 * -----------------------------------------------------------------------------
 */
package org.enhydra.jdbc.standard;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Hashtable;
import javax.sql.ConnectionEvent;
import java.sql.Connection;
import org.enhydra.jdbc.core.CoreConnection;
import org.enhydra.jdbc.util.LRUCache;

/**
 * This is an implementation of java.sql.Connection which simply
 * delegates almost everything to an underlying physical implemention
 * of the same interface.
 *
 * It relies on a StandardPooledConnection to create it and to supply the
 * physical connection and a cache of PreparedStatements. This class will
 * try to re-use PreparedStatements wherever possible and will add to the
 * cache when totally new PreparedStatements get created.
 */
public class StandardConnectionHandle extends CoreConnection {
    
    StandardPooledConnection 	pooledCon;		// the pooled connection that created this object
    protected Hashtable         masterPrepStmtCache;    // the hashtable of caches, indexed by physical connection
    int		preparedStmtCacheSize;  // the size of the connection-specific cache
    protected LRUCache		preparedStatementCache = null;	// prepared statements indexed by SQL string
    public Hashtable		inUse;			// prepared statements that are currently in use
    private boolean		closed;			// set true when this connection has been closed
    
    /**
     * Constructor.
     */
    public StandardConnectionHandle (StandardPooledConnection pooledCon, Hashtable preparedStatementCache, int preparedStmtCacheSize) {
	super(pooledCon.getPhysicalConnection());		// get the real connection
	this.pooledCon = pooledCon;				// first save parameters
	masterPrepStmtCache = preparedStatementCache;
	this.preparedStmtCacheSize = preparedStmtCacheSize;
	setupPreparedStatementCache();
	inUse = new Hashtable (10, 0.5f);
    } // Constructor

    protected void setupPreparedStatementCache() {
	if (preparedStmtCacheSize == 0) {
	    preparedStatementCache = null;
	    return;
	}
	preparedStatementCache = (LRUCache)masterPrepStmtCache.get(con.toString());
	if (preparedStatementCache == null) {
	    preparedStatementCache = new PreparedStatementCache(preparedStmtCacheSize);
	    masterPrepStmtCache.put(con.toString(), preparedStatementCache);
	}
    }
    
    /** 
     * Pre-invokation of the delegation, in case of connection is
     * closed, we throw an exception
     */
    public void preInvoke() throws SQLException {
	if (closed)
	    throw new SQLException ("Connection is closed");
    } // method preInvoke

    /**
     * Exception management : catch or throw the exception
     */
    public void catchInvoke(SQLException e) throws SQLException {
	//ConnectionEvent event = new ConnectionEvent (pooledCon);// create event associate with the connection
	//pooledCon.connectionErrorOccurred(event);		// ppoled have to be closed
	throw(e);						// throw the exception
    } // method catchInvoke

    /**
     * Closes this StandardConnectionHandle and prevents it
     * from being reused. It also returns used PreparedStatements
     * to the PreparedStatement cache and notifies all listeners.
     */
    synchronized public void close() throws SQLException {
	// Note - we don't check to see if already closed. Some servers get confused.
	closed=true;					// connection now closed
	Enumeration keys = inUse.keys();		// get any prepared statements in use
	while (keys.hasMoreElements()) {		// while more prepared statements used
	    Object key = keys.nextElement();		// get next key
	    returnToCache (key);			// return prepared statement to cache
	}
	pooledCon.closeEvent();				// notify listeners
    } // method close
    
    /**
     * Removes a prepared statement from the inUse list
     * and returns it to the cache.
     */
    void returnToCache (Object key, Connection theCon) {
	Object value = inUse.remove(key);			// remove key/value from used statements
	if  (value != null) {
	    LRUCache theCache = (LRUCache)
		masterPrepStmtCache.get(theCon.toString());
	    theCache.put (key, value);	// place back in cache, ready for re-use
	}
    } // method returnToCache

    void returnToCache (Object key) {
	returnToCache(key, con);
    }
    
    /**
     * Checks to see if a prepared statement with the same concurrency
     * has already been created. If not, then a new prepared statement
     * is created and added to the cache.
     *
     * If a prepared statement is found in the cache then it is removed 
     * from the cache and placed on the "inUse" list. This ensures that
     * if multiple threads use the same StandardConnectionHandle, or a single
     * thread does multiple prepares using the same SQL, then DIFFERENT
     * prepared statements will be returned.
     */
    synchronized PreparedStatement checkPreparedCache (String sql, int type, int concurrency) throws SQLException {
	PreparedStatement ret = null;				// the return value
	// NOTE - We include the Connection in the lookup key. This has no 
	// effect here but is needed by StandardXAConnection where the the physical 
	// Connection used can vary over time depending on the global transaction.
	String lookupKey = sql+type+concurrency;		// used to lookup statements
	if (preparedStatementCache != null) {
	    Object obj = preparedStatementCache.get (lookupKey);// see if there's a PreparedStatement already
	    if (obj != null) {									// if there is
		ret = (PreparedStatement)obj;		// use as return value
		try {
		    ret.clearParameters();			// make it look like new
		} catch (SQLException e) {
		    // Bad statement, so we have to create a new one
		    ret = createPreparedStatement(sql, type, concurrency);
		}
     
		preparedStatementCache.remove(lookupKey);	// make sure it cannot be re-used
		inUse.put (lookupKey, ret);			// make sure it gets reused by later delegates
	    } else {					// no PreparedStatement ready
		ret = createPreparedStatement(sql, type, concurrency);
		inUse.put (lookupKey, ret);			// will get saved in prepared statement cache
	    }
	} else {
	    ret = createPreparedStatement(sql, type, concurrency);
	}
	// We don't actually give the application a real PreparedStatement. Instead
	// they get a StandardPreparedStatement that delegates everything except
	// PreparedStatement.close();

	ret = new StandardPreparedStatement (this, ret, lookupKey);
	return ret;
    } // method checkPreparedCache

    protected PreparedStatement createPreparedStatement(String sql, int type, int concurrency) throws SQLException {
	if (type==0) {				// if no type or concurrency specified
	    return con.prepareStatement(sql);	// create new prepared statement
	} else {
	    return con.prepareStatement(sql, type, concurrency);// create new prepared statement
	}
    }
    
    /**
     * Creates a PreparedStatement for the given SQL. If possible, the
     * statement is fetched from the cache.
     */
    public PreparedStatement prepareStatement(String sql) throws SQLException {
	preInvoke();
	try {
	    return checkPreparedCache(sql, 0, 0);
	} catch (SQLException e) {
	    catchInvoke(e);
	}
	return null;
    } // method PreparedStatement

    /**
     * Creates a PreparedStatement for the given SQL, type and concurrency. 
     * If possible, the statement is fetched from the cache.
     */
    public PreparedStatement prepareStatement(String sql,int resultSetType,int resultSetConcurrency) throws SQLException {
	preInvoke();
	try {
	    return checkPreparedCache(sql, resultSetType, resultSetConcurrency);
	} catch (SQLException e) {
	    catchInvoke(e);
	}
	return null;
    } // prepareStatement
    
     public boolean isClosed() throws SQLException {
	return closed;
    } // method isClosed
    
    public CallableStatement prepareCall(String sql,int resultSetType,int resultSetConcurrency) throws SQLException {
	preInvoke();
	try {
	    return con.prepareCall (sql, resultSetType, resultSetConcurrency);
	} catch (SQLException e) {
	    catchInvoke(e);
	}
	return null;
    } //  method prepareCall
    
} // class StandardConnectionHandle
