/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.myfaces.orchestra.connectionManager;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Set;

/**
 * Manage all borrowed connections and hand out
 * {@link org.apache.myfaces.orchestra.connectionManager.DisconnectableConnection}
 * objects so that we can close them again after the HTTP request has been finished.
 * <p>
 * This datasource can be configured as a "wrapper" for a real datasource. When a connection is
 * requested from this object, a proxy is returned that simply forwards all calls transparently
 * to the real connection. This manager keeps track of all the Connections borrowed by each
 * thread. At some point (eg from a servlet filter) this object can be asked to check for 
 * unreturned connections held by the current thread. If any exist then the real connection
 * is returned to the underlying datasource and the proxy's connection reference is set to null.
 * This ensures that a thread cannot leak real database connections.
 * <p>
 * Of course all code should return its connections; this is only a workaround/hack useful when the
 * real problem cannot be fixed. This is particularly useful for JPA implementations that do not free
 * their connection again after a lazy-init.
 * <p>
 * If a proxy's underlying connection has been returned to the database (either via the
 * leak-detection, or by explicitly calling close) then invoking any method on the proxy
 * will transparently cause a new connection to be retrieved from the underlying datasource.
 * This means that a Connection returned by this datasource works somewhat differently than
 * a normal one: for a normal connection, close() followed by prepareStatement() would cause
 * an exception to be thrown, but works when this datasource is used.
 *
 * @see org.apache.myfaces.orchestra.connectionManager.DisconnectableConnection
 */
public class ConnectionManagerDataSource implements DataSource
{
    private DataSource dataSource;
    private String jndiName;

    // List of connections that have been borrowed by this thread but not returned.
    // When using a threadpool, it is required that the releaseAllBorrowedConnections
    // method be called before the thread is returned to the pool; that ensures this
    // threadlocal is reset to null.
    private static ThreadLocal borrowedConnections = new ThreadLocal()
    {
        protected Object initialValue()
        {
            return new HashSet();
        }
    };

    public ConnectionManagerDataSource()
    {
    }

    void onAfterBorrowConnection(Connection con)
    {
        ((Set) borrowedConnections.get()).add(con);
    }

    void onAfterReleaseConnection(Connection con)
    {
        ((Set) borrowedConnections.get()).remove(con);
    }

    /**
     * If the calling thread has allocated connections via this datasource, then return the
     * underlying real connections to the underlying datasource.
     * <p>
     * To code that holds references to the proxy connection returned by this datasource,
     * this operation is generally transparent. They continue to hold a reference to the
     * proxy, and if a method is ever called on that proxy in the future then the proxy
     * will transparently allocate a new underlying Connection at that time. 
     * <p>
     * This is expected to be called just before a thread is returned to a threadpool,
     * eg via a ServletFilter just before returning from processing a request.
     */
    public static void releaseAllBorrowedConnections()
    {
        DisconnectableConnection[] connections = new DisconnectableConnection[((Set) borrowedConnections.get()).size()];
        ((Set) borrowedConnections.get()).toArray(connections);

        for (int i = 0; i<connections.length; i++)
        {
            DisconnectableConnection connection = connections[i];
            connection.disconnect();
        }

        ((Set) borrowedConnections.get()).clear();
    }

    /**
     * Set the underlying datasource via an explicit call.
     * See also method setJndiName.
     */
    public void setDataSource(DataSource dataSource)
    {
        this.dataSource = dataSource;
    }

    /**
     * Return the underlying datasource for this wrapper.
     * <p>
     * If method setJndiName was used to specify the datasource, then it is retrieved
     * from JNDI if necessary.
     * 
     * @throws IllegalArgumentException if neither setDataSource nor setJndiName was called. 
     * @throws IllegalArgumentException if an invalid jndi name was specified.
     */
    public DataSource getDataSource()
    {
        if (dataSource != null)
        {
            return dataSource;
        }

        try
        {
            Context ctx = new InitialContext();
            dataSource = (DataSource) ctx.lookup(jndiName);
        }
        catch (NamingException e)
        {
            throw (IllegalArgumentException) new IllegalArgumentException(jndiName).initCause(e);
        }

        return dataSource;
    }

    /**
     * Specify that the underlying datasource should be retrieved via JNDI.
     */
    public void setJndiName(String jndiName)
    {
        this.jndiName = jndiName;
    }

    /**
     * Return a proxy that wraps a connection of the underlying datasource.
     */
    public Connection getConnection() throws SQLException
    {
        return DisconnectableConnectionFactory.create(this);
    }

    /**
     * Not supported. Always throws UnsupportedOperationException.
     */
    public Connection getConnection(String username, String password) throws SQLException
    {
        throw new UnsupportedOperationException();
    }

    /** @inheritDoc */
    public PrintWriter getLogWriter() throws SQLException
    {
        return getDataSource().getLogWriter();
    }

    /** @inheritDoc */
    public void setLogWriter(PrintWriter out) throws SQLException
    {
        getDataSource().setLogWriter(out);
    }

    /** @inheritDoc */
    public void setLoginTimeout(int seconds) throws SQLException
    {
        getDataSource().setLoginTimeout(seconds);
    }

    /** @inheritDoc */
    public int getLoginTimeout() throws SQLException
    {
        return getDataSource().getLoginTimeout();
    }

    /**
     * Always throws UnsupportedOperationException.
     * <p>
     * Note that this method was only introduced in java 1.6, and therefore
     * cannot be implemented on platforms earlier than this without using
     * reflection. Orchestra supports pre-1.6 JVMs, and this is a very
     * rarely used method so currently no support is offered for this
     * method.
     */
    public Object unwrap(Class iface) throws SQLException
    {
        throw new UnsupportedOperationException();
        /*
        try
        {
            if (iface.isAssignableFrom(dataSource.getClass()))
            {
                return dataSource;
            }

            Method method = dataSource.getClass().getMethod("unwrap", new Class[]{Class.class});
            return method.invoke(dataSource, new Object[] { iface });
        }
        catch (NoSuchMethodException e)
        {
            throw new UnsupportedOperationException();
        }
        catch (IllegalAccessException e)
        {
            throw new SQLException(e);
        }
        catch (InvocationTargetException e)
        {
            throw new SQLException(e);
        }
        */
    }

    /**
     * Always throws UnsupportedOperationException.
     * See method unwrap.
     */
    public boolean isWrapperFor(Class iface) throws SQLException
    {
        throw new UnsupportedOperationException();

        /*
        try
        {
            if (iface.isAssignableFrom(dataSource.getClass()))
            {
                return true;
            }
            Method method = dataSource.getClass().getMethod("isWrapperFor", new Class[]{Class.class});
            return Boolean.TRUE.equals(method.invoke(dataSource, new Object[] { iface }));
        }
        catch (NoSuchMethodException e)
        {
            throw new UnsupportedOperationException();
        }
        catch (IllegalAccessException e)
        {
            throw new SQLException(e);
        }
        catch (InvocationTargetException e)
        {
            throw new SQLException(e);
        }
        */
    }
}
