/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2007 Bull S.A.S.
 * Contact: jonas-team@ow2.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
 *
 * --------------------------------------------------------------------------
 * $Id: SQLConnectionInvocationHandler.java 21566 2011-08-08 12:28:12Z cazauxj $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.resource.internal.cm.sql;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.ow2.jonas.resource.internal.SQLManager;
import org.ow2.jonas.resource.internal.cm.ManagedConnectionInfo;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

/**
 * SQL Connection Wrapper
 *
 * @author Eric Hardesty
 * Contributor(s):
 */
public class SQLConnectionInvocationHandler implements InvocationHandler {

    /**
     * Number of fixed arguments
     */
    private static final int PROXY_FIXED_ARGS = 3;
    /**
     * Real connection object
     */
    private Object connection = null;
    /**
     * Connection manager holding the PreparedStatement cache
     */
    private SQLManager conman = null;
    /**
     * ManagedConnectionInfo for the connection
     */
    private ManagedConnectionInfo mci = null;
    /**
     * Userid of the connection
     */
    private String user = null;
    /**
     * Logger object
     */
    private Logger trace = null;

    /**
     * Determine if statement is still valid
     */
    private boolean invalid = false;
    /**
     * Determine if statement cache is in use
     */
    private boolean supportsPreparedCache = true;

    /**
     * PreparedStatement wrapper
     * @param pConn JDBC connection object
     * @param pMci ManagedConnectionInfo object associated with the connection
     * @param pConman SQLManager object holding PreparedStatement cache
     * @param pTrace Logger object to use
     * @throws Exception if an error occurs
     */
    public SQLConnectionInvocationHandler(Object pConn, ManagedConnectionInfo pMci, SQLManager pConman, Logger pTrace)
              throws Exception {
        connection = pConn;
        mci = pMci;
        conman = pConman;
        trace = pTrace;
        user = mci.mc.getMetaData().getUserName();
    }

    /**
     * Returns a proxy for the sql Connection
     *
     * @param pConn JDBC connection object
     * @param pMci ManagedConnectionInfo object associated with the connection
     * @param pConman SQLManager object holding PreparedStatement cache
     * @param pTrace Logger object to use
     * @return Object of the preparedStatement proxy
     * @throws Exception if an error occurs
     */
    public static Object createSQLWrapper(Object pConn, ManagedConnectionInfo pMci,
                                           SQLManager pConman, Logger pTrace)
                 throws Exception {
        Class class1 = pConn.getClass();
        Class [] aclass = buildInterfaces(class1);
        return Proxy.newProxyInstance(class1.getClassLoader(), aclass,
                                       new SQLConnectionInvocationHandler(pConn, pMci, pConman, pTrace));
    }

    /**
     * Invoke call on the proxy
     *
     * @param obj the proxy instance that the method was invoked on
     * @param method the Method instance
     * @param aobj an array of objects containing the values of the arguments
     * @throws Throwable if an error occurs
     * @return Object the returned from the method invocation on the proxy instance
     */
    public Object invoke(Object obj, Method method, Object [] aobj)
        throws Throwable {

        Object retObj = null;
        if (method.getName().compareTo("prepareStatement") == 0) {
            retObj = prepareStatement(method.getParameterTypes(), aobj);
            if (retObj == null) {
                retObj = method.invoke(connection, aobj);
            }
        } else {
            if (method.getName().compareTo("close") == 0) {
                invalid = true;
            } else if (method.getName().compareTo("toString") == 0) {
                return connection.toString();
            } else if (method.getName().compareTo("commit") == 0) {
                if (trace.isLoggable(BasicLevel.DEBUG)) {
                    trace.log(BasicLevel.DEBUG, "" + this);
                }
            } else {
                checkIfValid();
            }
            retObj = method.invoke(connection, aobj);
        }
        return retObj;
    }

    /**
     * Invoke correct preparedStatement
     * @param pTypes Class [] of parameter types
     * @param pValues Class [] of parameter values
     * @return Object the result of invoking the preparedStatement
     * @throws Exception if any Exception occurs
     */
    public Object prepareStatement(Class [] pTypes, Object [] pValues)
        throws Exception {

        checkIfValid();

        if (supportsPreparedCache) {
            try {
                Class [] mTypes = new Class[PROXY_FIXED_ARGS + pTypes.length];
                Object [] mValues = new Object[PROXY_FIXED_ARGS + pValues.length];

                mTypes[0] = ManagedConnectionInfo.class;
                mTypes[1] = Object.class;
                mTypes[2] = String.class;

                mValues[0] = mci;
                mValues[1] = connection;
                mValues[2] = user;

                // Loop should be faster than System.arraycopy for small
                // number of parameters
                for (int i = 0; i < pTypes.length; i++) {
                    mTypes[PROXY_FIXED_ARGS + i] = pTypes[i];
                    mValues[PROXY_FIXED_ARGS + i] = pValues[i];
                }

                Method meth1 = conman.getClass().getMethod("getPStatement", mTypes);
                return meth1.invoke(conman, mValues);
            } catch (Exception ex) {
                ex.printStackTrace();
                return null;
            }
        } else {
            return null;
        }

    }

    /**
     * Return a string describing this object
     * @return String representing this object
     */
    public String toString() {
        return connection.toString();
    }

    /**
     * Check if the wrapper is valid
     * @throws SQLException if the wrapper is invalid
     */
    private void checkIfValid() throws SQLException {
        if (invalid) {
            throw new SQLException("Connection is closed");
        }
    }

    /* Utility methods to build interfaces*/
    /**
     * Build the class interfaces from the class object
     * @param class1 Class to retrieve the interfaces
     * @return Class [] all interfaces
     */
    private static Class[] buildInterfaces(Class class1) {
        ArrayList arlist = new ArrayList();
        addToList(class1, arlist);
        Class [] aclass = new Class[arlist.size()];
        return (Class []) arlist.toArray(aclass);
    }

    /**
     * Add each of the interfaces
     * @param cls Class to determine what interfaces are implemented
     * @param lst List of interfaces
     */
    private static void addToList(Class cls, List lst) {
        Class [] aclass = cls.getInterfaces();
        for (int i = 0; i < aclass.length; i++) {
            if (!lst.contains(aclass[i])) {
                lst.add(aclass[i]);
            }
            addToList(aclass[i], lst);
        }

        Class class2 = cls.getSuperclass();
        if (class2 != null) {
            addToList(class2, lst);
        }
    }
}
