/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2009 France Telecom R&D
 * 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 (at your option) 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.jndi.interceptors.impl.datasource;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.sql.DataSource;

import org.ow2.jonas.jndi.checker.api.IResourceChecker;
import org.ow2.jonas.jndi.checker.api.IResourceCheckerInfo;
import org.ow2.jonas.jndi.checker.api.ResourceCheckpoints;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * Wrapper of existing datasources. Each call to getConnection(*) is kept in
 * order to check if close have been done when detect method is called.
 * @author Florent Benoit
 */
public class DatasourceWrapper implements DataSource, IResourceChecker {

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(DatasourceWrapper.class);

    /**
     * Internal counter for wrapped connections.
     */
    private Long count = Long.valueOf(0);

    /**
     * Close on check ?
     */
    private boolean forceClose = true;

    /**
     * Wrapped datasource.
     */
    private DataSource wrappedDataSource = null;

    /**
     * Map of connections that are opened. [id] ---> [invocationHandler].
     */
    private Map<Long, ConnectionProxy> openConnectionsMap = null;

    /**
     * Wrap a given datasource.
     * @param wrappedDataSource the given datasource
     */
    public DatasourceWrapper(final DataSource wrappedDataSource) {
        this.openConnectionsMap = new HashMap<Long, ConnectionProxy>();
        this.wrappedDataSource = wrappedDataSource;
    }

    /**
     * <p>
     * Attempts to establish a connection with the data source that this
     * <code>DataSource</code> object represents.
     * @return a connection to the data source
     * @exception java.sql.SQLException if a database access error occurs
     */
    public Connection getConnection() throws SQLException {

        // Wrap the connection
        return wrapConnection(wrappedDataSource.getConnection(), Thread.currentThread().getStackTrace());
    }

    /**
     * <p>
     * Attempts to establish a connection with the data source that this
     * <code>DataSource</code> object represents.
     * @param username the database user on whose behalf the connection is being
     *        made
     * @param password the user's password
     * @return a connection to the data source
     * @exception SQLException if a database access error occurs
     */
    public Connection getConnection(final String username, final String password) throws SQLException {

        // And then call the wrapped datasource
        return wrapConnection(wrappedDataSource.getConnection(username, password), Thread.currentThread().getStackTrace());
    }

    /**
     * Wrap the given connection and return it to the client.
     * @param connection the connection to wrap
     * @param stackTraceElements the stack trace
     * @return the wrapped connection
     */
    protected synchronized Connection wrapConnection(final Connection connection, final StackTraceElement[] stackTraceElements) {

        // Remove some elements of the stack trace (like
        // ava.lang.Thread.dumpThreads(Native Method),
        // java.lang.Thread.getStackTrace(Thread.java:1383), etc)
        List<StackTraceElement> listStack = new ArrayList<StackTraceElement>();
        boolean wrapperFound = false;
        for (StackTraceElement stackTraceElement : stackTraceElements) {
            // Not yet found, don't add
            if (wrapperFound) {
                listStack.add(stackTraceElement);
            }
            // Matching item, add the next stack elements
            if (DatasourceWrapper.class.getName().equals(stackTraceElement.getClassName())) {
                wrapperFound = true;
            }
        }
        StackTraceElement[] neWStackTraceElements = listStack.toArray(new StackTraceElement[listStack.size()]);

        // Wrap connection
        ConnectionProxy connectionProxy = new ConnectionProxy(count++, this, connection, neWStackTraceElements);

        // Add this handler to the list of opened
        openConnectionsMap.put(connectionProxy.getId(), connectionProxy);

        // return the proxy
        return (Connection) Proxy.newProxyInstance(wrappedDataSource.getClass().getClassLoader(),
                new Class[] {Connection.class}, connectionProxy);
    }

    /**
     * Remove the data for the given connection id.
     * @param id the connection ID
     */
    protected synchronized void remove(final Long id) {
        openConnectionsMap.remove(id);
    }

    /**
     * Callback occuring in order to check if there are problems.
     * @param resourceCheckerInfo some data for the resource checker
     */
    public synchronized void detect(final IResourceCheckerInfo resourceCheckerInfo) {

        // CheckPoint of the check (post_invoke, after_http, etc.
        ResourceCheckpoints checkPoint = resourceCheckerInfo.getCheckPoint();

        // Needs to check if there are connections that are still open
        Set<Long> keySet = openConnectionsMap.keySet();
        Iterator<Long> itKeys = keySet.iterator();
        while (itKeys.hasNext()) {
            Long key = itKeys.next();
            ConnectionProxy connectionProxy = openConnectionsMap.get(key);

            // Print that the connection will be automatically closed
            if (forceClose) {
                logger.warn("JDBC connection not closed by the caller, close has been forced by the server. Stack trace of the getConnection() call is ''{0}''. Additional info ''{1}''", Arrays.asList(connectionProxy.getStackTraceElements()).toString().replace(",", "\n"), resourceCheckerInfo.getCallerInfo());
            } else {
                // Connection leak but not closed automatically
                logger.warn("JDBC connection not closed by the caller. Stack trace of the getConnection() call is ''{0}''. Additional info ''{1}''", Arrays.asList(connectionProxy.getStackTraceElements()).toString().replace(",", "\n"), resourceCheckerInfo.getCallerInfo());
            }


            if (forceClose) {
                try {
                    connectionProxy.getWrappedConnection().close();
                } catch (SQLException e) {
                    logger.error("Unable to force the close of the JDBC connection", e);
                }
            }
        }

    }

    /**
     * <p>
     * Retrieves the log writer for this <code>DataSource</code> object.
     * <p>
     * The log writer is a character output stream to which all logging and
     * tracing messages for this data source 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 <code>java.sql.Drivermanager</code> class.
     * When a <code>DataSource</code> object is created, the log writer is
     * initially null; in other words, the default is for logging to be
     * disabled.
     * @return the log writer for this data source or null if logging is disabled
     * @exception SQLException if a database access error occurs
     * @see #setLogWriter
     */
    public java.io.PrintWriter getLogWriter() throws SQLException {
        return wrappedDataSource.getLogWriter();
    }

    /**
     * <p>
     * Sets the log writer for this <code>DataSource</code> object to the given
     * <code>java.io.PrintWriter</code> object.
     * <p>
     * The log writer is a character output stream to which all logging and
     * tracing messages for this data source 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 <code>java.sql.Drivermanager</code> class.
     * When a <code>DataSource</code> object is created the log writer is
     * initially null; in other words, the default is for logging to be
     * disabled.
     * @param out the new log writer; to disable logging, set to null
     * @exception SQLException if a database access error occurs
     * @see #getLogWriter
     */
    public void setLogWriter(final java.io.PrintWriter out) throws SQLException {
        wrappedDataSource.setLogWriter(out);
    }

    /**
     * <p>
     * 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 <code>DataSource</code> 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.
     * @see #getLoginTimeout
     */
    public void setLoginTimeout(final int seconds) throws SQLException {
        wrappedDataSource.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 <code>DataSource</code> object is
     * created, the login timeout is initially zero.
     * @return the data source login time limit
     * @exception SQLException if a database access error occurs.
     * @see #setLoginTimeout
     */
    public int getLoginTimeout() throws SQLException {
        return wrappedDataSource.getLoginTimeout();
    }

    /**
     * Method implemented by some datasources.
     * @return the optional mapper name
     */
    public String getMapperName() {
        Method getMapperNameMethod = null;
        try {
            getMapperNameMethod = wrappedDataSource.getClass().getMethod("getMapperName");
        } catch (NoSuchMethodException e) {
            return null;
        }
        try {
            return (String) getMapperNameMethod.invoke(wrappedDataSource);
        } catch (IllegalAccessException e) {
            return null;
        } catch (InvocationTargetException e) {
            return null;
        }

    }

    /**
     * Sets the flag for automatically closing or not the connection.
     * @param forceClose the given boolean value
     */
    public void setForceClose(final boolean forceClose) {
        this.forceClose = forceClose;
    }



    /**
     * Change equals method to use the equals on the underlying wrapped object.
     * For example jorm is comparing the datasource objects and if they're different, some merge is not occuring.
     * @param other the other object to compare
     * @return true if objects are equals.
     */
    @Override
    public boolean equals(final Object other) {
        if (this == wrappedDataSource) {
            return true;
        }
        if ((other == null) || (other.getClass() != this.getClass())) {
            return false;
        }
        DatasourceWrapper otherDatasource = (DatasourceWrapper) other;
        return wrappedDataSource.equals(otherDatasource.wrappedDataSource);
    }

    /**
     * @return the hashCode of the wrapped object.
     */
    @Override
    public int hashCode() {
        return wrappedDataSource.hashCode();
    }

}
