package org.postgresql.jdbc2.optional;

import javax.sql.*;
import java.sql.*;
import java.util.*;
import java.lang.reflect.*;
import org.postgresql.PGConnection;

/**
 * PostgreSQL implementation of the PooledConnection interface.  This shouldn't
 * be used directly, as the pooling client should just interact with the
 * ConnectionPool instead.
 * @see ConnectionPool
 *
 * @author Aaron Mulder (ammulder@chariotsolutions.com)
 * @author Csaba Nagy (ncsaba@yahoo.com)
 * @version $Revision: 1.7 $
 */
public class PooledConnectionImpl implements PooledConnection
{
	private List listeners = new LinkedList();
	private Connection con;
	private ConnectionHandler last;
	private boolean autoCommit;

	/**
	 * Creates a new PooledConnection representing the specified physical
	 * connection.
	 */
	protected PooledConnectionImpl(Connection con, boolean autoCommit)
	{
		this.con = con;
		this.autoCommit = autoCommit;
	}

	/**
	 * Adds a listener for close or fatal error events on the connection
	 * handed out to a client.
	 */
	public void addConnectionEventListener(ConnectionEventListener connectionEventListener)
	{
		listeners.add(connectionEventListener);
	}

	/**
	 * Removes a listener for close or fatal error events on the connection
	 * handed out to a client.
	 */
	public void removeConnectionEventListener(ConnectionEventListener connectionEventListener)
	{
		listeners.remove(connectionEventListener);
	}

	/**
	 * Closes the physical database connection represented by this
	 * PooledConnection.  If any client has a connection based on
	 * this PooledConnection, it is forcibly closed as well.
	 */
	public void close() throws SQLException
	{
		if (last != null)
		{
			last.close();
			if (!con.getAutoCommit())
			{
				try
				{
					con.rollback();
				}
				catch (SQLException e)
				{}
			}
		}
		try
		{
			con.close();
		}
		finally
		{
			con = null;
		}
	}

	/**
	 * Gets a handle for a client to use.  This is a wrapper around the
	 * physical connection, so the client can call close and it will just
	 * return the connection to the pool without really closing the
	 * pgysical connection.
	 *
	 * <p>According to the JDBC 2.0 Optional Package spec (6.2.3), only one
	 * client may have an active handle to the connection at a time, so if
	 * there is a previous handle active when this is called, the previous
	 * one is forcibly closed and its work rolled back.</p>
	 */
	public Connection getConnection() throws SQLException
	{
		if (con == null)
		{
			// Before throwing the exception, let's notify the registered listeners about the error
			final SQLException sqlException = new SQLException("This PooledConnection has already been closed!");
			fireConnectionFatalError(sqlException);
			throw sqlException;
		}
		// If any error occures while opening a new connection, the listeners
		// have to be notified. This gives a chance to connection pools to
		// elliminate bad pooled connections.
		try
		{
			// Only one connection can be open at a time from this PooledConnection.  See JDBC 2.0 Optional Package spec section 6.2.3
			if (last != null)
			{
				last.close();
				if (!con.getAutoCommit())
				{
					try
					{
						con.rollback();
					}
					catch (SQLException e)
					{}
				}
				con.clearWarnings();
			}
			con.setAutoCommit(autoCommit);
		}
		catch (SQLException sqlException)
		{
			fireConnectionFatalError(sqlException);
			throw (SQLException)sqlException.fillInStackTrace();
		}
		ConnectionHandler handler = new ConnectionHandler(con);
		last = handler;
		Connection con = (Connection)Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{Connection.class, PGConnection.class}, handler);
		last.setProxy(con);
		return con;
	}

	/**
	 * Used to fire a connection closed event to all listeners.
	 */
	void fireConnectionClosed()
	{
		ConnectionEvent evt = null;
		// Copy the listener list so the listener can remove itself during this method call
		ConnectionEventListener[] local = (ConnectionEventListener[]) listeners.toArray(new ConnectionEventListener[listeners.size()]);
		for (int i = 0; i < local.length; i++)
		{
			ConnectionEventListener listener = local[i];
			if (evt == null)
			{
				evt = new ConnectionEvent(this);
			}
			listener.connectionClosed(evt);
		}
	}

	/**
	 * Used to fire a connection error event to all listeners.
	 */
	void fireConnectionFatalError(SQLException e)
	{
		ConnectionEvent evt = null;
		// Copy the listener list so the listener can remove itself during this method call
		ConnectionEventListener[] local = (ConnectionEventListener[])listeners.toArray(new ConnectionEventListener[listeners.size()]);
		for (int i = 0; i < local.length; i++)
		{
			ConnectionEventListener listener = local[i];
			if (evt == null)
			{
				evt = new ConnectionEvent(this, e);
			}
			listener.connectionErrorOccurred(evt);
		}
	}

	/**
	 * Instead of declaring a class implementing Connection, which would have
	 * to be updated for every JDK rev, use a dynamic proxy to handle all
	 * calls through the Connection interface.	This is the part that
	 * requires JDK 1.3 or higher, though JDK 1.2 could be supported with a
	 * 3rd-party proxy package.
	 */
	private class ConnectionHandler implements InvocationHandler
	{
		private Connection con;
        private Connection proxy; // the Connection the client is currently using, which is a proxy
		private boolean automatic = false;

		public ConnectionHandler(Connection con)
		{
			this.con = con;
		}

		public Object invoke(Object proxy, Method method, Object[] args)
		throws Throwable
		{
			// From Object
			if (method.getDeclaringClass().getName().equals("java.lang.Object"))
			{
				if (method.getName().equals("toString"))
				{
					return "Pooled connection wrapping physical connection " + con;
				}
				if (method.getName().equals("hashCode"))
				{
					return new Integer(con.hashCode());
				}
				if (method.getName().equals("equals"))
				{
					if (args[0] == null)
					{
						return Boolean.FALSE;
					}
					try
					{
						return Proxy.isProxyClass(args[0].getClass()) && ((ConnectionHandler) Proxy.getInvocationHandler(args[0])).con == con ? Boolean.TRUE : Boolean.FALSE;
					}
					catch (ClassCastException e)
					{
						return Boolean.FALSE;
					}
				}
                                try
                                {
                                    return method.invoke(con, args);
                                }
                                catch (InvocationTargetException e)
                                {
                                    throw e.getTargetException();
                                }
			}
			// All the rest is from the Connection or PGConnection interface
			if (method.getName().equals("isClosed"))
			{
				return con == null ? Boolean.TRUE : Boolean.FALSE;
			}
			if (con == null)
			{
				throw new SQLException(automatic ? "Connection has been closed automatically because a new connection was opened for the same PooledConnection or the PooledConnection has been closed" : "Connection has been closed");
			}
			if (method.getName().equals("close"))
			{
				SQLException ex = null;
				if (!con.getAutoCommit())
				{
					try
					{
						con.rollback();
					}
					catch (SQLException e)
					{
						ex = e;
					}
				}
				con.clearWarnings();
				con = null;
                proxy = null;
				last = null;
				fireConnectionClosed();
				if (ex != null)
				{
					throw ex;
				}
				return null;
			}
            else if(method.getName().equals("createStatement"))
            {
                Statement st = (Statement)method.invoke(con, args);
                return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{Statement.class}, new StatementHandler(this, st));
            }
            else if(method.getName().equals("prepareCall"))
            {
                Statement st = (Statement)method.invoke(con, args);
                return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{CallableStatement.class}, new StatementHandler(this, st));
            }
            else if(method.getName().equals("prepareStatement"))
            {
                Statement st = (Statement)method.invoke(con, args);
                return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{PreparedStatement.class}, new StatementHandler(this, st));
            }
			else
			{
				return method.invoke(con, args);
			}
		}

        Connection getProxy() {
            return proxy;
        }

        void setProxy(Connection proxy) {
            this.proxy = proxy;
        }

		public void close()
		{
			if (con != null)
			{
				automatic = true;
			}
			con = null;
            proxy = null;
			// No close event fired here: see JDBC 2.0 Optional Package spec section 6.3
		}

        public boolean isClosed() {
            return con == null;
        }
	}

    /**
     * Instead of declaring classes implementing Statement, PreparedStatement,
     * and CallableStatement, which would have to be updated for every JDK rev,
     * use a dynamic proxy to handle all calls through the Statement
     * interfaces.	This is the part that requires JDK 1.3 or higher, though
     * JDK 1.2 could be supported with a 3rd-party proxy package.
     *
     * The StatementHandler is required in order to return the proper
     * Connection proxy for the getConnection method.
     */
    private static class StatementHandler implements InvocationHandler {
        private ConnectionHandler con;
        private Statement st;

        public StatementHandler(ConnectionHandler con, Statement st) {
            this.con = con;
            this.st = st;
        }
        public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable
        {
            // From Object
            if (method.getDeclaringClass().getName().equals("java.lang.Object"))
            {
                if (method.getName().equals("toString"))
                {
                    return "Pooled statement wrapping physical statement " + st;
                }
                if (method.getName().equals("hashCode"))
                {
                    return new Integer(st.hashCode());
                }
                if (method.getName().equals("equals"))
                {
                    if (args[0] == null)
                    {
                        return Boolean.FALSE;
                    }
                    try
                    {
                        return Proxy.isProxyClass(args[0].getClass()) && ((StatementHandler) Proxy.getInvocationHandler(args[0])).st == st ? Boolean.TRUE : Boolean.FALSE;
                    }
                    catch (ClassCastException e)
                    {
                        return Boolean.FALSE;
                    }
                }
                return method.invoke(st, args);
            }
            // All the rest is from the Statement interface
            if (st == null || con.isClosed())
            {
                throw new SQLException("Statement has been closed");
            }
            if (method.getName().equals("close"))
            {
                try {
                    st.close();
                } finally {
                    con = null;
                    st = null;
                    return null;
                }
            }
            else if (method.getName().equals("getConnection"))
            {
                return con.getProxy(); // the proxied connection, not a physical connection
            }
            else
            {
                try
                {
                    return method.invoke(st, args);
                }
                catch (InvocationTargetException e)
                {
                    throw e.getTargetException();
                }
            }
        }
    }
}
