/**
 * 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.objectweb.jonas.jndi.interceptors.impl.datasource;

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

import javax.sql.DataSource;

import org.objectweb.jonas.jndi.checker.IResourceChecker;
import org.objectweb.jonas.jndi.checker.IResourceCheckerInfo;
import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.dbm.ConnectionManager;
import org.objectweb.jonas.jdbc.DataSourceImpl;
import org.objectweb.util.monolog.api.Logger;
import org.objectweb.util.monolog.api.BasicLevel;

/**
 * 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 Logger logger = Log.getLogger(DatasourceWrapper.class.getName());

    /**
     * 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(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(), 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(String username, String password)
            throws SQLException {


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

    private StackTraceElement[] getStackTrace() {
        return new Throwable().getStackTrace();
    }


    /**
     * Wrap the given connection and return it to the client
     * @param connection the connection to wrap
     * @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 (int i = 0; i < stackTraceElements.length; i++) {
            StackTraceElement stackTraceElement = stackTraceElements[i];
            // 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 = (StackTraceElement[]) listStack.toArray(new StackTraceElement[listStack.size()]); 

        // Wrap connection
        ConnectionProxy connectionProxy = new ConnectionProxy(increment(),
                                                              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);
    }

    private synchronized Long increment() {
    	count = new Long(count.longValue() + 1);
		return count;
	}

    /**
     * Remove the data for the given connection id.
     * @param id the connection ID
     */
    protected synchronized void remove(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(IResourceCheckerInfo resourceCheckerInfo) {

        // 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 = (Long) itKeys.next();
            ConnectionProxy connectionProxy = (ConnectionProxy) openConnectionsMap.get(key);
            if (logger.isLoggable(BasicLevel.WARN)) {
            	logger.log(BasicLevel.WARN,
            			"A connection has not been closed. Flag for enforcing the close is set to '"
            			+ forceClose + "', Stack trace of the getConnection() call is '"
            			+ Arrays.asList(connectionProxy.getStackTraceElements()) + "'. Additional info '"
            			+ resourceCheckerInfo.getCallerInfo() + "'");
            }
            if (forceClose) {
                try {
                    connectionProxy.getWrappedConnection().close();
                } catch (SQLException e) {
                    logger.log(BasicLevel.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(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(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();
    }

    public String getMapperName() {

        // DBM
        if (wrappedDataSource instanceof ConnectionManager) {
            return ((ConnectionManager) wrappedDataSource).getMapperName();
        }

        // RA JDBC
        if (wrappedDataSource instanceof DataSourceImpl) {
            return ((DataSourceImpl) wrappedDataSource).getMapperName();
        }

        // Default mapper name
        return "rdb";
    }

    /**
     * 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.
     */
    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.
     */
    public int hashCode() {
        return wrappedDataSource.hashCode();
    }

}