/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2005 Bull S.A.
 * 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: ManagedConnectionFactoryImpl.java 21496 2011-06-27 14:43:10Z sauthieg $
 * --------------------------------------------------------------------------
 */
package org.ow2.jonas.ee.jdbc;

import java.io.ByteArrayInputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;

import javax.resource.ResourceException;
import javax.resource.spi.ConnectionManager;
import javax.resource.spi.ConnectionRequestInfo;
import javax.resource.spi.ManagedConnection;
import javax.resource.spi.ManagedConnectionFactory;
import javax.resource.spi.SecurityException;
import javax.resource.spi.ValidatingManagedConnectionFactory;
import javax.resource.spi.security.PasswordCredential;
import javax.security.auth.Subject;

import org.ow2.jonas.lib.util.Log;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.objectweb.util.monolog.api.LoggerFactory;
import org.objectweb.util.monolog.wrapper.printwriter.LoggerImpl;

/**
 * @author Eric hardesty
 */
public abstract class ManagedConnectionFactoryImpl implements ManagedConnectionFactory, 
                                                              ValidatingManagedConnectionFactory,
                                                              Serializable {

    // Factory config data property
    MCFData mcfData = null;

    int hashcode = 0;

    PrintWriter pw;

    String logTopic = "";

    protected static final String LOGGER_FACTORY = "org.objectweb.util.monolog.loggerFactory";

    public Logger trace = null;

    /**
     * Debug level ?
     */
    private boolean isEnabledDebug = false;

    /**
     * connection check level
     */
    private int checkLevel = 0;

    /**
     * jdbc test statement
     */
    private String testStmt = null;

    /**
     * Constants for use with JDBC connection level
     */
    public final static int JDBC_NO_TEST = 0;
    public final static int JDBC_CHECK_CONNECTION = 1;
    public final static int JDBC_SEND_STATEMENT = 2;
    public final static int JDBC_KEEP_ALIVE = 3;

    public ManagedConnectionFactoryImpl() {
        pw = null;
        mcfData = new MCFData();
    }

    /* Abstract methods, specific implementation is different
     * for each type of MCF
     */
    public abstract ManagedConnection createManagedConnection(Subject subject, ConnectionRequestInfo cxReq)
        throws ResourceException;

    public abstract boolean equals(Object obj);

    /* Common methods for each MCF type
     */
    public Object createConnectionFactory() throws ResourceException {
        return new DataSourceImpl(this, null);
    }

    public Object createConnectionFactory(ConnectionManager cxMgr) throws ResourceException {
        return new DataSourceImpl(this, cxMgr);
    }

    public void getLogger(String _logTopic) throws Exception {
        if (trace == null || !(logTopic.equals(_logTopic))) {
            logTopic = _logTopic; // set the log topic value
            // Get the trace module:
            try {
                LoggerFactory mf = Log.getLoggerFactory();
                if (logTopic != null && logTopic.length() > 0) {
                    trace = mf.getLogger(logTopic);
                } else if (pw != null) {
                    trace = new LoggerImpl(pw);
                } else {
                    trace = mf.getLogger("org.ow2.jonas.ee.jdbc.RA");
                }
            } catch (Exception ex) {
                try {
                    if (pw != null) {
                        trace = new LoggerImpl(pw);
                    }
                } catch (Exception e3) {
                    throw new Exception("Cannot get logger");
                }
            }
        }
        isEnabledDebug = trace.isLoggable(BasicLevel.DEBUG);
        if (isEnabledDebug) {
            trace.log(BasicLevel.DEBUG, "getLogger(" + _logTopic + ")");
        }
    }

    public PrintWriter getLogWriter() throws ResourceException {
        return pw;
    }

    public int hashCode() {
        if (hashcode == 0) {
            hashcode = mcfData.hashCode();
            try {
                getLogger(mcfData.getMCFData(MCFData.LOGTOPIC));
            } catch (Exception ex) {
            }
        }

        return hashcode;
    }

    public ManagedConnection matchManagedConnections(Set connectionSet, Subject subject, ConnectionRequestInfo cxReq)
        throws ResourceException {
        if (isEnabledDebug) {
            trace.log(BasicLevel.DEBUG, "matchManagedConnection(" + connectionSet + "," + subject + "," + cxReq + ")");
        }
        if (connectionSet == null) {
            return null;
        }
        javax.resource.spi.security.PasswordCredential pc = getPasswordCredential(this, subject, cxReq, pw);
        Iterator it = connectionSet.iterator();
        Object obj = null;
        ManagedConnectionImpl mc = null;
        while (it.hasNext()) {
            try {
                obj = it.next();
                if (obj instanceof ManagedConnectionImpl) {
                    ManagedConnectionImpl mc1 = (ManagedConnectionImpl) obj;
                    if (pc == null && equals(mc1.mcf)) {
                        mc = mc1;
                        break;
                    }
                    if (pc != null && pc.equals(mc1.pc)) {
                        mc = mc1;
                        break;
                    }
                }
            } catch (Exception ex) {
                throw new ResourceException(ex.getMessage(), ex);
            }
        }
        if (isEnabledDebug) {
            trace.log(BasicLevel.DEBUG, "matchManagedConnection returns(" + mc + ")");
        }
        return mc;
    }

    public void setLogWriter(PrintWriter _pw) throws ResourceException {
        pw = _pw;
    }

    /* Common getter/setters */
    public String getDbSpecificMethods() {
        return mcfData.getMCFData(MCFData.DBSPECIFICMETHODS);
    }

    public void setDbSpecificMethods(String val) {
        mcfData.setMCFData(MCFData.DBSPECIFICMETHODS, val);
    }

    public String getDsClass() {
        return mcfData.getMCFData(MCFData.DSCLASS);
    }

    public void setDsClass(String val) {
        mcfData.setMCFData(MCFData.DSCLASS, val);
    }

    public String getIsolationLevel() {
        String str = mcfData.getMCFData(MCFData.ISOLATIONLEVEL);
        String retStr = "default";

        if (str.length() == 0 || str.equals("-1")) {
            return retStr;
        }

        int isolationLevel;
        try {
            isolationLevel = Integer.parseInt(str);
        } catch (Exception ex) {
            return retStr;
        }

        if (isolationLevel == Connection.TRANSACTION_SERIALIZABLE) {
            retStr = "serializable";
        } else if (isolationLevel == Connection.TRANSACTION_NONE) {
            retStr = "none";
        } else if (isolationLevel == Connection.TRANSACTION_READ_COMMITTED) {
            retStr = "read_committed";
        } else if (isolationLevel == Connection.TRANSACTION_READ_UNCOMMITTED) {
            retStr = "read_uncommitted";
        } else if (isolationLevel == Connection.TRANSACTION_REPEATABLE_READ) {
            retStr = "repeatable_read";
        }
        return retStr;
    }

    public void setIsolationLevel(String val) {
        int isolationLevel = -1;

        if (val.equals("serializable")) {
            isolationLevel = Connection.TRANSACTION_SERIALIZABLE;
        } else if (val.equals("none")) {
            isolationLevel = Connection.TRANSACTION_NONE;
        } else if (val.equals("read_committed")) {
            isolationLevel = Connection.TRANSACTION_READ_COMMITTED;
        } else if (val.equals("read_uncommitted")) {
            isolationLevel = Connection.TRANSACTION_READ_UNCOMMITTED;
        } else if (val.equals("repeatable_read")) {
            isolationLevel = Connection.TRANSACTION_REPEATABLE_READ;
        }

        mcfData.setMCFData(MCFData.ISOLATIONLEVEL, "" + isolationLevel);
    }

    public String getLoginTimeout() {
        return mcfData.getMCFData(MCFData.LOGINTIMEOUT);
    }

    public void setLoginTimeout(String val) {
        mcfData.setMCFData(MCFData.LOGINTIMEOUT, val);
    }

    public String getLogTopic() {
        return mcfData.getMCFData(MCFData.LOGTOPIC);
    }

    public void setLogTopic(String val) {
        mcfData.setMCFData(MCFData.LOGTOPIC, val);
        try {
            getLogger(val.trim());
        } catch (Exception ex) {
        }
    }

    public String getMapperName() {
        return mcfData.getMCFData(MCFData.MAPPERNAME);
    }

    public void setMapperName(String val) {
        mcfData.setMCFData(MCFData.MAPPERNAME, val);
    }

    public String getPassword() {
        return mcfData.getMCFData(MCFData.PASSWORD);
    }

    public void setPassword(String val) {
        mcfData.setMCFData(MCFData.PASSWORD, val);
    }

    public String getUser() {
        return mcfData.getMCFData(MCFData.USER);
    }

    public void setUser(String val) {
        mcfData.setMCFData(MCFData.USER, val);
    }

    /**
     * Set the level of checking on jdbc Connections
     * @param lev 0=no check, 1=ckeck open, 2=try stm, 3=keep alive
     */
    public void setJdbcConnLevel(int lev) {
        if (isEnabledDebug) {
            trace.log(BasicLevel.DEBUG, "checkLevel =" + checkLevel);
        }
        checkLevel =  lev;
    }

    public int getJdbcConnLevel() {
        return checkLevel;
    }

    /**
     * Set the tesqt statement to use, in case of check level 2 or 3.
     * @stmt sql statement
     */
    public void setJdbcConnTestStmt(String stmt) {
        if (isEnabledDebug) {
            trace.log(BasicLevel.DEBUG, "testStmt =" + testStmt);
        }
        testStmt = stmt;
    }

    public String getJdbcConnTestStmt() {
        return testStmt;
    }

    /**
     * This method returns a set of invalid ManagedConnection objects chosen from 
     * a specified set of ManagedConnection objects.
     * @param set of ManagedConnection to test
     * @return Set of invalid Connections from the given set
     * @throws ResourceException The operation failed
     */
    public Set getInvalidConnections(Set connectionSet) throws ResourceException {
        Set ret = new HashSet();
        if (checkLevel != JDBC_KEEP_ALIVE) {
            return ret;
        }
        if (testStmt == null || testStmt.length() == 0) {
            trace.log(BasicLevel.WARN, "jdbc-test-statement not set in jonas-ra.xml");
            return ret;
        }
        if (isEnabledDebug) {
            trace.log(BasicLevel.DEBUG, "Keep Alive Connections");
        }
        Iterator it = connectionSet.iterator();
        while (it.hasNext()) {
            try {
                ManagedConnectionImpl mci = (ManagedConnectionImpl) it.next();
                if (! mci.isValid()) {
                    ret.add(mci);
                }
            } catch (ClassCastException e) {
                trace.log(BasicLevel.DEBUG, "ClassCastException", e);
            } catch (Exception e) {
                trace.log(BasicLevel.DEBUG, "exc:", e);
            }
        }
        return ret;
    }

    /**
     * Returns the Password credential
     * @param mcf ManagedConnectionFactory currently being used
     * @param subject Subject associated with this call
     * @param info ConnectionRequestInfo
     * @return PasswordCredential for this user
     */
    protected static synchronized PasswordCredential getPasswordCredential(ManagedConnectionFactory mcf, Subject subject,
                                                                           ConnectionRequestInfo info, java.io.PrintWriter out) throws ResourceException {

        if (subject == null) {
            if (info == null) {
                return null;
            }
            ConnectionRequestInfoImpl crii = (ConnectionRequestInfoImpl) info;
            PasswordCredential pc = new PasswordCredential(crii.user, crii.password.toCharArray());
            pc.setManagedConnectionFactory(mcf);
            return pc;
        }
        Set cred = subject.getPrivateCredentials(PasswordCredential.class);
        PasswordCredential pc = null;
        for (Iterator iter = cred.iterator(); iter.hasNext();) {
            PasswordCredential tmpPc = (PasswordCredential) iter.next();
            if (tmpPc.getManagedConnectionFactory().equals(mcf)) {
                pc = tmpPc;
                break;
            }
        }
        if (pc == null) {
            SecurityException se = new SecurityException("No PasswordCredential found");
            out.println("" + se);
            throw se;
        }
        return pc;
    }

    protected static synchronized Object getDataSource(ManagedConnectionFactoryImpl mcf, PasswordCredential pc, Logger trace)
        throws ResourceException {

        MCFData prop = mcf.mcfData;

        String clsName = prop.getMCFData(MCFData.DSCLASS);
        if (clsName == null || clsName.length() == 0) {
            ResourceException re = new ResourceException("A DataSource (dsClass) value must be specified");
            trace.log(BasicLevel.INFO, re.getMessage());
            throw re;
        }
        Class dsClass = null;
        Object dsObj = null;
        try {
            dsClass = Class.forName(clsName, true, Thread.currentThread().getContextClassLoader());
            dsObj = dsClass.newInstance();
            if (trace.isLoggable(BasicLevel.DEBUG)) {
                trace.log(BasicLevel.DEBUG, "dsClass(" + clsName + ") is " + dsObj);
            }
        } catch (ClassNotFoundException cnfe) {
            trace.log(BasicLevel.WARN, "Class name not found:" + clsName);
            throw new ResourceException("Class Name not found:" + clsName, cnfe);
        } catch (Exception ex) {
            trace.log(BasicLevel.WARN, "Error in class:" + clsName);
            throw new ResourceException("Error in class: " + clsName + " " + ex.getMessage(), ex);
        }

        Object[] param = new Object[1];
        String methodName = null;
        String paramVal = null;
        Method meth = null;
        for (Enumeration e = prop.getProperties(); e.hasMoreElements();) {
            int offset = Integer.parseInt((String) e.nextElement());
            methodName = MCFData.dsMethodNames[offset];
            if (trace.isLoggable(BasicLevel.DEBUG)) {
                trace.log(BasicLevel.DEBUG, "Method: " + methodName);
            }
            if (!(methodName.equals("setDSClass") || methodName.equals("setDbSpecificMethods") || offset > MCFData.JONASOFFSET)) {
                try {
                    /* Determine if a String method exist */
                    paramVal = prop.getProperty("" + offset);
                    if (trace.isLoggable(BasicLevel.DEBUG)) {
                        trace.log(BasicLevel.DEBUG, "calling method " + methodName + " with String " + paramVal);
                    }
                    meth = dsClass.getMethod(methodName, new Class[] {String.class});
                    param[0] = paramVal;
                    meth.invoke(dsObj, param);
                    if (trace.isLoggable(BasicLevel.DEBUG)) {
                        trace.log(BasicLevel.DEBUG, "called method " + methodName + " with String " + paramVal);
                    }
                } catch (NoSuchMethodException ns) {
                    try {
                        /* Valid case, determine if an Integer method exist */
                        if (trace.isLoggable(BasicLevel.DEBUG)) {
                            trace.log(BasicLevel.DEBUG, "calling method " + methodName + " with int " + paramVal);
                        }
                        meth = dsClass.getMethod(methodName, new Class[] {int.class});
                        param[0] = new Integer(paramVal);
                        meth.invoke(dsObj, param);
                        if (trace.isLoggable(BasicLevel.DEBUG)) {
                            trace.log(BasicLevel.DEBUG, "called method " + methodName + " with int " + paramVal);
                        }
                    } catch (NoSuchMethodException nsme) {
                        // Valid case no String or Integer method continue thru
                        // remainder of properties
                        if (trace.isLoggable(BasicLevel.DEBUG)) {
                            trace.log(BasicLevel.DEBUG, "No such method " + methodName + " with " + paramVal);
                        }
                    } catch (NumberFormatException nfm) {
                        // Valid case cannot convert to an Integer
                        if (trace.isLoggable(BasicLevel.DEBUG)) {
                            trace.log(BasicLevel.DEBUG, "Not found method " + methodName + " with " + paramVal);
                        }
                    } catch (Exception ex0) {
                        ex0.printStackTrace();
                        throw new ResourceException("Error on method: " + methodName + " " + ex0.getMessage(), ex0);
                    }
                } catch (IllegalArgumentException iae) {
                    ;
                } catch (Exception ex) {
                    ex.printStackTrace();
                    throw new ResourceException("Error on method: " + methodName + " " + ex.getMessage(), ex);
                }
            }
            if (methodName.equals("setDbSpecificMethods")) {
                Vector meths = new Vector();
                Vector methParams = new Vector();
                Vector methTypes = new Vector();
                try {
                    parseValues(prop.getProperty("" + offset), meths, methParams, methTypes, trace);
                } catch (Exception ex1) {
                    throw new ResourceException("Error parsing dbSpecificMethods: " + ex1.getMessage());
                }
                if (meths != null && meths.size() > 0) {
                    for (int i = 0; i < meths.size(); i++) {
                        try {
                            /* Determine if a String method exist */
                            methodName = (String) meths.elementAt(i);
                            Class toPass = null;
                            String curMethType = (String) methTypes.elementAt(i);
                            if (curMethType.equalsIgnoreCase("String")) {
                                toPass = String.class;
                                param[0] = (String) methParams.elementAt(i);
                            } else if (curMethType.equalsIgnoreCase("Integer")) {
                                toPass = Integer.class;
                                param[0] = Integer.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equalsIgnoreCase("int")) {
                                toPass = int.class;
                                param[0] = Integer.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("Float")) {
                                toPass = Float.class;
                                param[0] = Float.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("float")) {
                                toPass = float.class;
                                param[0] = Float.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("Boolean")) {
                                toPass = Boolean.class;
                                param[0] = Boolean.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("boolean")) {
                                toPass = boolean.class;
                                param[0] = Boolean.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equalsIgnoreCase("Character")) {
                                toPass = Character.class;
                                param[0] = new Character(((String) methParams.elementAt(i)).charAt(0));
                            } else if (curMethType.equalsIgnoreCase("char")) {
                                toPass = char.class;
                                param[0] = new Character(((String) methParams.elementAt(i)).charAt(0));
                            } else if (curMethType.equals("Double")) {
                                toPass = Double.class;
                                param[0] = Double.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("double")) {
                                toPass = double.class;
                                param[0] = Double.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("Byte")) {
                                toPass = Byte.class;
                                param[0] = Byte.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("byte")) {
                                toPass = byte.class;
                                param[0] = Byte.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("Short")) {
                                toPass = Short.class;
                                param[0] = Short.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("short")) {
                                toPass = short.class;
                                param[0] = Short.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("Long")) {
                                toPass = Long.class;
                                param[0] = Long.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("long")) {
                                toPass = long.class;
                                param[0] = Long.valueOf((String) methParams.elementAt(i));
                            } else if (curMethType.equals("Properties") || curMethType.equals("java.lang.Properties")) {
                                toPass = Properties.class;
                                param[0] = buildProperties((String) methParams.elementAt(i), trace);
                            }
                            if (trace.isLoggable(BasicLevel.DEBUG)) {
                                trace.log(BasicLevel.DEBUG, "calling method " + methodName + " with " + param[0]);
                            }
                            meth = dsClass.getMethod(methodName, new Class[] {toPass});
                            meth.invoke(dsObj, param);
                        } catch (NoSuchMethodException ns) {
                            throw new ResourceException("No such method: " + methodName + " " + ns.getMessage(), ns);
                        } catch (Exception ex) {
                            ex.printStackTrace();
                            throw new ResourceException("Error on method: " + methodName + " " + ex.getMessage(), ex);
                        }
                    }
                }
            }
        }
        if (pc != null) {
            try {
                param[0] = pc.getUserName();
                meth = dsClass.getMethod("setUserName", new Class[] {Class.forName("String")});
                meth.invoke(dsObj, param);

                param[0] = new String(pc.getPassword());
                meth = dsClass.getMethod("setPassword", new Class[] {Class.forName("String")});
                meth.invoke(dsObj, param);
            } catch (Exception ex) {
                throw new ResourceException("Error on method: " + methodName + " " + ex.getMessage(), ex);
            }
        }

        if (trace.isLoggable(BasicLevel.DEBUG)) {
            try {
                meth = dsClass.getMethod("getURL", (Class[]) null);
                trace.log(BasicLevel.DEBUG, "URL is " + meth.invoke(dsObj, (Object[]) null));
            } catch (Exception e) {
            }
        }
        return dsObj;

    }

    private static Properties buildProperties(String val, Logger trace)
        throws Exception {
        if (trace.isLoggable(BasicLevel.DEBUG)) {
            trace.log(BasicLevel.DEBUG, "" + val);
        }
        Properties ret = new Properties();

        if (val.length() == 0) {
            return ret;
        }
        String pairs = val.substring(1, val.length()-1);
        String parseVal = pairs.replace(',', '\n');
        try {
            ByteArrayInputStream valStream = new ByteArrayInputStream(parseVal.getBytes());
            ret.load(valStream);
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new Exception("Invalid value specified for Properties parameter in dbSpecificMethods");
        }

        return ret;
    }

    private static void parseValues(String val, Vector vMeth, Vector vValues, Vector vTypes, Logger trace)
        throws Exception {
        char delim = ':';
        boolean done = false;
        String methName = null;
        int offset = 0;
        boolean parsed = false;
        String parseVal = val.trim();
        String typeVal = "";
        String valVal = "";

        if (parseVal.length() == 0) {
            return;
        }
        if (parseVal.startsWith(":")) {
            delim = parseVal.charAt(1);
            parseVal = parseVal.substring(2);
        }
        while (!parsed) {
            offset = parseVal.indexOf('=');
            if (offset < 0) {
                throw new Exception("Invalid value specified for dbSpecificMethods");
            }
            methName = parseVal.substring(0, offset);
            vMeth.add(methName);
            parseVal = parseVal.substring(offset + 1);
            if (parseVal.charAt(0) == delim) {
                valVal = "";
            } else {
                offset = parseVal.indexOf(delim);
                if (offset < 0) {
                    valVal = parseVal;
                    offset = valVal.length() - 1;
                } else {
                    valVal = parseVal.substring(0, offset);
                }
            }
            vValues.add(valVal);
            if (offset < 0) {
                parsed = true;
            } else {
                parseVal = parseVal.substring(offset + 1);
                if (parseVal.length() == 0) {
                    parsed = true;
                }
            }
            if (parseVal.startsWith("" + delim)) {
                parseVal = parseVal.substring(1);
                offset = parseVal.indexOf(delim);
                if (offset < 0) {
                    typeVal = parseVal;
                } else {
                    typeVal = parseVal.substring(0, offset);
                }
                vTypes.add(typeVal);
                if (offset < 0) {
                    parsed = true;
                } else {
                    parseVal = parseVal.substring(offset + 1);
                    if (parseVal.length() == 0) {
                        parsed = true;
                    }
                }
            } else {
                vTypes.add("String");
            }
            if (trace.isLoggable(BasicLevel.DEBUG)) {
                trace.log(BasicLevel.DEBUG, "Parsed: method(" + methName + ") value(" + valVal + ") type(" + typeVal + ")");
            }
        }
    }
}
