/*
 * 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.tomcat.dbcp.dbcp2;

import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.ClientInfoStatus;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Struct;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;

/**
 * A base delegating implementation of {@link Connection}.
 * <p>
 * All of the methods from the {@link Connection} interface simply check to see that the {@link Connection} is active,
 * and call the corresponding method on the "delegate" provided in my constructor.
 * </p>
 * <p>
 * Extends AbandonedTrace to implement Connection tracking and logging of code which created the Connection. Tracking
 * the Connection ensures that the AbandonedObjectPool can close this connection and recycle it if its pool of
 * connections is nearing exhaustion and this connection's last usage is older than the removeAbandonedTimeout.
 * </p>
 *
 * @param <C>
 *            the Connection type
 *
 * @since 2.0
 */
public class DelegatingConnection<C extends Connection> extends AbandonedTrace implements Connection {

    private static final Map<String, ClientInfoStatus> EMPTY_FAILED_PROPERTIES = Collections
            .<String, ClientInfoStatus>emptyMap();

    /** My delegate {@link Connection}. */
    private volatile C connection;

    private volatile boolean closed;

    private boolean cacheState = true;
    private Boolean autoCommitCached;
    private Boolean readOnlyCached;
    private Integer defaultQueryTimeoutSeconds;

    /**
     * Creates a wrapper for the Connection which traces this Connection in the AbandonedObjectPool.
     *
     * @param c
     *            the {@link Connection} to delegate all calls to.
     */
    public DelegatingConnection(final C c) {
        super();
        connection = c;
    }

    /**
     * Returns a string representation of the metadata associated with the innermost delegate connection.
     */
    @Override
    public synchronized String toString() {
        String s = null;

        final Connection c = this.getInnermostDelegateInternal();
        if (c != null) {
            try {
                if (c.isClosed()) {
                    s = "connection is closed";
                } else {
                    final StringBuffer sb = new StringBuffer();
                    sb.append(hashCode());
                    final DatabaseMetaData meta = c.getMetaData();
                    if (meta != null) {
                        sb.append(", URL=");
                        sb.append(meta.getURL());
                        sb.append(", UserName=");
                        sb.append(meta.getUserName());
                        sb.append(", ");
                        sb.append(meta.getDriverName());
                        s = sb.toString();
                    }
                }
            } catch (final SQLException ex) {
                // Ignore
            }
        }

        if (s == null) {
            s = super.toString();
        }

        return s;
    }

    /**
     * Returns my underlying {@link Connection}.
     *
     * @return my underlying {@link Connection}.
     */
    public C getDelegate() {
        return getDelegateInternal();
    }

    protected final C getDelegateInternal() {
        return connection;
    }

    /**
     * Compares innermost delegate to the given connection.
     *
     * @param c
     *            connection to compare innermost delegate with
     * @return true if innermost delegate equals <code>c</code>
     */
    public boolean innermostDelegateEquals(final Connection c) {
        final Connection innerCon = getInnermostDelegateInternal();
        if (innerCon == null) {
            return c == null;
        }
        return innerCon.equals(c);
    }

    /**
     * If my underlying {@link Connection} is not a {@code DelegatingConnection}, returns it, otherwise recursively
     * invokes this method on my delegate.
     * <p>
     * Hence this method will return the first delegate that is not a {@code DelegatingConnection}, or {@code null} when
     * no non-{@code DelegatingConnection} delegate can be found by traversing this chain.
     * </p>
     * <p>
     * This method is useful when you may have nested {@code DelegatingConnection}s, and you want to make sure to obtain
     * a "genuine" {@link Connection}.
     * </p>
     *
     * @return innermost delegate.
     */
    public Connection getInnermostDelegate() {
        return getInnermostDelegateInternal();
    }

    /**
     * Although this method is public, it is part of the internal API and should not be used by clients. The signature
     * of this method may change at any time including in ways that break backwards compatibility.
     *
     * @return innermost delegate.
     */
    public final Connection getInnermostDelegateInternal() {
        Connection c = connection;
        while (c != null && c instanceof DelegatingConnection) {
            c = ((DelegatingConnection<?>) c).getDelegateInternal();
            if (this == c) {
                return null;
            }
        }
        return c;
    }

    /**
     * Sets my delegate.
     *
     * @param connection
     *            my delegate.
     */
    public void setDelegate(final C connection) {
        this.connection = connection;
    }

    /**
     * Closes the underlying connection, and close any Statements that were not explicitly closed. Sub-classes that
     * override this method must:
     * <ol>
     * <li>Call passivate()</li>
     * <li>Call close (or the equivalent appropriate action) on the wrapped connection</li>
     * <li>Set _closed to <code>false</code></li>
     * </ol>
     */
    @Override
    public void close() throws SQLException {
        if (!closed) {
            closeInternal();
        }
    }

    protected boolean isClosedInternal() {
        return closed;
    }

    protected void setClosedInternal(final boolean closed) {
        this.closed = closed;
    }

    protected final void closeInternal() throws SQLException {
        try {
            passivate();
        } finally {
            if (connection != null) {
                boolean connectionIsClosed;
                try {
                    connectionIsClosed = connection.isClosed();
                } catch (final SQLException e) {
                    // not sure what the state is, so assume the connection is open.
                    connectionIsClosed = false;
                }
                try {
                    // DBCP-512: Avoid exceptions when closing a connection in mutli-threaded use case.
                    // Avoid closing again, which should be a no-op, but some drivers like H2 throw an exception when
                    // closing from multiple threads.
                    if (!connectionIsClosed) {
                        connection.close();
                    }
                } finally {
                    closed = true;
                }
            } else {
                closed = true;
            }
        }
    }

    protected void handleException(final SQLException e) throws SQLException {
        throw e;
    }

    private void initializeStatement(final DelegatingStatement ds) throws SQLException {
        if (defaultQueryTimeoutSeconds != null && defaultQueryTimeoutSeconds.intValue() != ds.getQueryTimeout()) {
            ds.setQueryTimeout(defaultQueryTimeoutSeconds.intValue());
        }
    }

    @Override
    public Statement createStatement() throws SQLException {
        checkOpen();
        try {
            final DelegatingStatement ds = new DelegatingStatement(this, connection.createStatement());
            initializeStatement(ds);
            return ds;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public Statement createStatement(final int resultSetType, final int resultSetConcurrency) throws SQLException {
        checkOpen();
        try {
            final DelegatingStatement ds = new DelegatingStatement(this,
                    connection.createStatement(resultSetType, resultSetConcurrency));
            initializeStatement(ds);
            return ds;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public PreparedStatement prepareStatement(final String sql) throws SQLException {
        checkOpen();
        try {
            final DelegatingPreparedStatement dps = new DelegatingPreparedStatement(this,
                    connection.prepareStatement(sql));
            initializeStatement(dps);
            return dps;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency)
            throws SQLException {
        checkOpen();
        try {
            final DelegatingPreparedStatement dps = new DelegatingPreparedStatement(this,
                    connection.prepareStatement(sql, resultSetType, resultSetConcurrency));
            initializeStatement(dps);
            return dps;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public CallableStatement prepareCall(final String sql) throws SQLException {
        checkOpen();
        try {
            final DelegatingCallableStatement dcs = new DelegatingCallableStatement(this, connection.prepareCall(sql));
            initializeStatement(dcs);
            return dcs;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency)
            throws SQLException {
        checkOpen();
        try {
            final DelegatingCallableStatement dcs = new DelegatingCallableStatement(this,
                    connection.prepareCall(sql, resultSetType, resultSetConcurrency));
            initializeStatement(dcs);
            return dcs;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public void clearWarnings() throws SQLException {
        checkOpen();
        try {
            connection.clearWarnings();
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    @Override
    public void commit() throws SQLException {
        checkOpen();
        try {
            connection.commit();
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    /**
     * Returns the state caching flag.
     *
     * @return the state caching flag
     */
    public boolean getCacheState() {
        return cacheState;
    }

    @Override
    public boolean getAutoCommit() throws SQLException {
        checkOpen();
        if (cacheState && autoCommitCached != null) {
            return autoCommitCached.booleanValue();
        }
        try {
            autoCommitCached = Boolean.valueOf(connection.getAutoCommit());
            return autoCommitCached.booleanValue();
        } catch (final SQLException e) {
            handleException(e);
            return false;
        }
    }

    @Override
    public String getCatalog() throws SQLException {
        checkOpen();
        try {
            return connection.getCatalog();
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public DatabaseMetaData getMetaData() throws SQLException {
        checkOpen();
        try {
            return new DelegatingDatabaseMetaData(this, connection.getMetaData());
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public int getTransactionIsolation() throws SQLException {
        checkOpen();
        try {
            return connection.getTransactionIsolation();
        } catch (final SQLException e) {
            handleException(e);
            return -1;
        }
    }

    @Override
    public Map<String, Class<?>> getTypeMap() throws SQLException {
        checkOpen();
        try {
            return connection.getTypeMap();
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public SQLWarning getWarnings() throws SQLException {
        checkOpen();
        try {
            return connection.getWarnings();
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public boolean isReadOnly() throws SQLException {
        checkOpen();
        if (cacheState && readOnlyCached != null) {
            return readOnlyCached.booleanValue();
        }
        try {
            readOnlyCached = Boolean.valueOf(connection.isReadOnly());
            return readOnlyCached.booleanValue();
        } catch (final SQLException e) {
            handleException(e);
            return false;
        }
    }

    @Override
    public String nativeSQL(final String sql) throws SQLException {
        checkOpen();
        try {
            return connection.nativeSQL(sql);
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public void rollback() throws SQLException {
        checkOpen();
        try {
            connection.rollback();
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    /**
     * Gets the default query timeout that will be used for {@link Statement}s created from this connection.
     * <code>null</code> means that the driver default will be used.
     *
     * @return query timeout limit in seconds; zero means there is no limit.
     */
    public Integer getDefaultQueryTimeout() {
        return defaultQueryTimeoutSeconds;
    }

    /**
     * Sets the default query timeout that will be used for {@link Statement}s created from this connection.
     * <code>null</code> means that the driver default will be used.
     *
     * @param defaultQueryTimeoutSeconds
     *            the new query timeout limit in seconds; zero means there is no limit
     */
    public void setDefaultQueryTimeout(final Integer defaultQueryTimeoutSeconds) {
        this.defaultQueryTimeoutSeconds = defaultQueryTimeoutSeconds;
    }

    /**
     * Sets the state caching flag.
     *
     * @param cacheState
     *            The new value for the state caching flag
     */
    public void setCacheState(final boolean cacheState) {
        this.cacheState = cacheState;
    }

    /**
     * Can be used to clear cached state when it is known that the underlying connection may have been accessed
     * directly.
     */
    public void clearCachedState() {
        autoCommitCached = null;
        readOnlyCached = null;
        if (connection instanceof DelegatingConnection) {
            ((DelegatingConnection<?>) connection).clearCachedState();
        }
    }

    @Override
    public void setAutoCommit(final boolean autoCommit) throws SQLException {
        checkOpen();
        try {
            connection.setAutoCommit(autoCommit);
            if (cacheState) {
                autoCommitCached = Boolean.valueOf(autoCommit);
            }
        } catch (final SQLException e) {
            autoCommitCached = null;
            handleException(e);
        }
    }

    @Override
    public void setCatalog(final String catalog) throws SQLException {
        checkOpen();
        try {
            connection.setCatalog(catalog);
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    @Override
    public void setReadOnly(final boolean readOnly) throws SQLException {
        checkOpen();
        try {
            connection.setReadOnly(readOnly);
            if (cacheState) {
                readOnlyCached = Boolean.valueOf(readOnly);
            }
        } catch (final SQLException e) {
            readOnlyCached = null;
            handleException(e);
        }
    }

    @Override
    public void setTransactionIsolation(final int level) throws SQLException {
        checkOpen();
        try {
            connection.setTransactionIsolation(level);
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    @Override
    public void setTypeMap(final Map<String, Class<?>> map) throws SQLException {
        checkOpen();
        try {
            connection.setTypeMap(map);
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    @Override
    public boolean isClosed() throws SQLException {
        return closed || connection == null || connection.isClosed();
    }

    protected void checkOpen() throws SQLException {
        if (closed) {
            if (null != connection) {
                String label = "";
                try {
                    label = connection.toString();
                } catch (final Exception ex) {
                    // ignore, leave label empty
                }
                throw new SQLException("Connection " + label + " is closed.");
            }
            throw new SQLException("Connection is null.");
        }
    }

    protected void activate() {
        closed = false;
        setLastUsed();
        if (connection instanceof DelegatingConnection) {
            ((DelegatingConnection<?>) connection).activate();
        }
    }

    protected void passivate() throws SQLException {
        // The JDBC spec requires that a Connection close any open
        // Statement's when it is closed.
        // DBCP-288. Not all the traced objects will be statements
        final List<AbandonedTrace> traces = getTrace();
        if (traces != null && traces.size() > 0) {
            final Iterator<AbandonedTrace> traceIter = traces.iterator();
            while (traceIter.hasNext()) {
                final Object trace = traceIter.next();
                if (trace instanceof Statement) {
                    ((Statement) trace).close();
                } else if (trace instanceof ResultSet) {
                    // DBCP-265: Need to close the result sets that are
                    // generated via DatabaseMetaData
                    ((ResultSet) trace).close();
                }
            }
            clearTrace();
        }
        setLastUsed(0);
    }

    @Override
    public int getHoldability() throws SQLException {
        checkOpen();
        try {
            return connection.getHoldability();
        } catch (final SQLException e) {
            handleException(e);
            return 0;
        }
    }

    @Override
    public void setHoldability(final int holdability) throws SQLException {
        checkOpen();
        try {
            connection.setHoldability(holdability);
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    @Override
    public Savepoint setSavepoint() throws SQLException {
        checkOpen();
        try {
            return connection.setSavepoint();
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public Savepoint setSavepoint(final String name) throws SQLException {
        checkOpen();
        try {
            return connection.setSavepoint(name);
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public void rollback(final Savepoint savepoint) throws SQLException {
        checkOpen();
        try {
            connection.rollback(savepoint);
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    @Override
    public void releaseSavepoint(final Savepoint savepoint) throws SQLException {
        checkOpen();
        try {
            connection.releaseSavepoint(savepoint);
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    @Override
    public Statement createStatement(final int resultSetType, final int resultSetConcurrency,
            final int resultSetHoldability) throws SQLException {
        checkOpen();
        try {
            final DelegatingStatement ds = new DelegatingStatement(this,
                    connection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability));
            initializeStatement(ds);
            return ds;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency,
            final int resultSetHoldability) throws SQLException {
        checkOpen();
        try {
            final DelegatingPreparedStatement dps = new DelegatingPreparedStatement(this,
                    connection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability));
            initializeStatement(dps);
            return dps;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency,
            final int resultSetHoldability) throws SQLException {
        checkOpen();
        try {
            final DelegatingCallableStatement dcs = new DelegatingCallableStatement(this,
                    connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability));
            initializeStatement(dcs);
            return dcs;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys) throws SQLException {
        checkOpen();
        try {
            final DelegatingPreparedStatement dps = new DelegatingPreparedStatement(this,
                    connection.prepareStatement(sql, autoGeneratedKeys));
            initializeStatement(dps);
            return dps;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public PreparedStatement prepareStatement(final String sql, final int columnIndexes[]) throws SQLException {
        checkOpen();
        try {
            final DelegatingPreparedStatement dps = new DelegatingPreparedStatement(this,
                    connection.prepareStatement(sql, columnIndexes));
            initializeStatement(dps);
            return dps;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public PreparedStatement prepareStatement(final String sql, final String columnNames[]) throws SQLException {
        checkOpen();
        try {
            final DelegatingPreparedStatement dps = new DelegatingPreparedStatement(this,
                    connection.prepareStatement(sql, columnNames));
            initializeStatement(dps);
            return dps;
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public boolean isWrapperFor(final Class<?> iface) throws SQLException {
        if (iface.isAssignableFrom(getClass())) {
            return true;
        } else if (iface.isAssignableFrom(connection.getClass())) {
            return true;
        } else {
            return connection.isWrapperFor(iface);
        }
    }

    @Override
    public <T> T unwrap(final Class<T> iface) throws SQLException {
        if (iface.isAssignableFrom(getClass())) {
            return iface.cast(this);
        } else if (iface.isAssignableFrom(connection.getClass())) {
            return iface.cast(connection);
        } else {
            return connection.unwrap(iface);
        }
    }

    @Override
    public Array createArrayOf(final String typeName, final Object[] elements) throws SQLException {
        checkOpen();
        try {
            return connection.createArrayOf(typeName, elements);
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public Blob createBlob() throws SQLException {
        checkOpen();
        try {
            return connection.createBlob();
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public Clob createClob() throws SQLException {
        checkOpen();
        try {
            return connection.createClob();
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public NClob createNClob() throws SQLException {
        checkOpen();
        try {
            return connection.createNClob();
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public SQLXML createSQLXML() throws SQLException {
        checkOpen();
        try {
            return connection.createSQLXML();
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public Struct createStruct(final String typeName, final Object[] attributes) throws SQLException {
        checkOpen();
        try {
            return connection.createStruct(typeName, attributes);
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public boolean isValid(final int timeoutSeconds) throws SQLException {
        if (isClosed()) {
            return false;
        }
        try {
            return connection.isValid(timeoutSeconds);
        } catch (final SQLException e) {
            handleException(e);
            return false;
        }
    }

    @Override
    public void setClientInfo(final String name, final String value) throws SQLClientInfoException {
        try {
            checkOpen();
            connection.setClientInfo(name, value);
        } catch (final SQLClientInfoException e) {
            throw e;
        } catch (final SQLException e) {
            throw new SQLClientInfoException("Connection is closed.", EMPTY_FAILED_PROPERTIES, e);
        }
    }

    @Override
    public void setClientInfo(final Properties properties) throws SQLClientInfoException {
        try {
            checkOpen();
            connection.setClientInfo(properties);
        } catch (final SQLClientInfoException e) {
            throw e;
        } catch (final SQLException e) {
            throw new SQLClientInfoException("Connection is closed.", EMPTY_FAILED_PROPERTIES, e);
        }
    }

    @Override
    public Properties getClientInfo() throws SQLException {
        checkOpen();
        try {
            return connection.getClientInfo();
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public String getClientInfo(final String name) throws SQLException {
        checkOpen();
        try {
            return connection.getClientInfo(name);
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public void setSchema(final String schema) throws SQLException {
        checkOpen();
        try {
            Jdbc41Bridge.setSchema(connection, schema);
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    @Override
    public String getSchema() throws SQLException {
        checkOpen();
        try {
            return Jdbc41Bridge.getSchema(connection);
        } catch (final SQLException e) {
            handleException(e);
            return null;
        }
    }

    @Override
    public void abort(final Executor executor) throws SQLException {
        checkOpen();
        try {
            Jdbc41Bridge.abort(connection, executor);
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    @Override
    public void setNetworkTimeout(final Executor executor, final int milliseconds) throws SQLException {
        checkOpen();
        try {
            Jdbc41Bridge.setNetworkTimeout(connection, executor, milliseconds);
        } catch (final SQLException e) {
            handleException(e);
        }
    }

    @Override
    public int getNetworkTimeout() throws SQLException {
        checkOpen();
        try {
            return Jdbc41Bridge.getNetworkTimeout(connection);
        } catch (final SQLException e) {
            handleException(e);
            return 0;
        }
    }
}
