/*
 * Copyright 2004-2014 the Seasar Foundation and the Others.
 *
 * Licensed 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.seasar.extension.dbcp.impl;

import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
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.Map;
import java.util.Properties;
import java.util.concurrent.Executor;

import javax.sql.ConnectionEvent;
import javax.sql.ConnectionEventListener;
import javax.sql.XAConnection;
import javax.transaction.Transaction;
import javax.transaction.xa.XAResource;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.seasar.extension.dbcp.ConnectionPool;
import org.seasar.extension.dbcp.ConnectionWrapper;

/**
 * {@link ConnectionWrapper}の実装クラスです。
 *
 * @author higa
 *
 */
public class ConnectionWrapperImpl implements ConnectionWrapper, ConnectionEventListener {

    /** LOGGER */
    private static final Logger LOGGER = LogManager.getLogger(ConnectionWrapperImpl.class);

    /** XAResource */
    private final XAResource xaResource;
    /** ConnectionPool */
    private final ConnectionPool connectionPool;

    /** XAConnection */
    private XAConnection xaConnection;
    /** Connection */
    private Connection physicalConnection;
    /** クローズ判断 */
    private boolean closed = false;
    /** Transaction */
    private Transaction transaction;

    /**
     * {@link ConnectionWrapperImpl}を作成します。
     *
     * @param xac XAコネクション
     * @param conn 物理コネクション
     * @param pool コネクションプール
     * @param tx トランザクション
     * @throws SQLException SQL例外が発生した場合
     */
    public ConnectionWrapperImpl(final XAConnection xac, final Connection conn,
            final ConnectionPool pool, final Transaction tx) throws SQLException {
        this.xaConnection = xac;
        this.physicalConnection = conn;
        this.xaResource = new XAResourceWrapperImpl(xac.getXAResource(), this);
        this.connectionPool = pool;
        this.transaction = tx;
        xac.addConnectionEventListener(this);
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionWrapper#getPhysicalConnection()
     */
    @Override
    public Connection getPhysicalConnection() {
        return this.physicalConnection;
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionWrapper#getXAResource()
     */
    @Override
    public XAResource getXAResource() {
        return this.xaResource;
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionWrapper#getXAConnection()
     */
    @Override
    public XAConnection getXAConnection() {
        return this.xaConnection;
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionWrapper#init(javax.transaction.Transaction)
     */
    @Override
    public void init(final Transaction tx) {
        this.closed = false;
        this.transaction = tx;
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionWrapper#cleanup()
     */
    @Override
    public void cleanup() {
        this.xaConnection.removeConnectionEventListener(this);
        this.closed = true;
        this.xaConnection = null;
        this.physicalConnection = null;
        this.transaction = null;
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionWrapper#closeReally()
     */
    @Override
    public void closeReally() {
        if (this.xaConnection == null) {
            return;
        }
        this.closed = true;

        try {
            if (!this.physicalConnection.isClosed()) {
                if (!this.physicalConnection.getAutoCommit()) {
                    try {
                        this.physicalConnection.rollback();
                        this.physicalConnection.setAutoCommit(true);
                    } catch (final SQLException ex) {
                        LOGGER.warn(ex);
                    }
                }
                this.physicalConnection.close();
            }
        } catch (final SQLException ex) {
            LOGGER.warn(ex);
        } finally {
            this.physicalConnection = null;
        }

        try {
            this.xaConnection.close();
            LOGGER.info("xaConnection is closed.");
        } catch (final SQLException ex) {
            LOGGER.warn(ex);
        } finally {
            this.xaConnection = null;
        }
    }

    /**
     * コネクション確認
     * @throws SQLException SQL例外
     */
    private void assertOpened() throws SQLException {
        if (this.closed) {
            throw new SQLException("connection is close.");
        }
    }

    /**
     * トランザクション確認
     * @throws SQLException SQL例外
     */
    private void assertLocalTx() throws SQLException {
        if (this.transaction != null) {
            throw new SQLException("local transaction is not available.");
        }
    }

    /**
     * @see org.seasar.extension.dbcp.ConnectionWrapper#release()
     */
    @Override
    public void release() {
        if (!this.closed) {
            this.connectionPool.release(this);
        }
    }

    /**
     * @see javax.sql.ConnectionEventListener#connectionClosed(javax.sql.ConnectionEvent)
     */
    @Override
    public void connectionClosed(final ConnectionEvent event) {
        return;
    }

    /**
     * @see javax.sql.ConnectionEventListener#connectionErrorOccurred(javax.sql.ConnectionEvent)
     */
    @Override
    public void connectionErrorOccurred(final ConnectionEvent event) {
        release();
    }

    /**
     * @see java.sql.Connection#createStatement()
     */
    @Override
    public Statement createStatement() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.createStatement();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#prepareStatement(java.lang.String)
     */
    @Override
    public PreparedStatement prepareStatement(final String sql) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.prepareStatement(sql);
        } catch (final SQLException ex) {
            release();
            LOGGER.info(sql);
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#prepareCall(java.lang.String)
     */
    @Override
    public CallableStatement prepareCall(final String sql) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.prepareCall(sql);
        } catch (final SQLException ex) {
            release();
            LOGGER.info(sql);
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#nativeSQL(java.lang.String)
     */
    @Override
    public String nativeSQL(final String sql) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.nativeSQL(sql);
        } catch (final SQLException ex) {
            release();
            LOGGER.info(sql);
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#isClosed()
     */
    @Override
    public boolean isClosed() {
        return this.closed;
    }

    /**
     * @see java.sql.Connection#getMetaData()
     */
    @Override
    public DatabaseMetaData getMetaData() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.getMetaData();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#setReadOnly(boolean)
     */
    @Override
    public void setReadOnly(final boolean readOnly) throws SQLException {
        assertOpened();
        try {
            this.physicalConnection.setReadOnly(readOnly);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#isReadOnly()
     */
    @Override
    public boolean isReadOnly() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.isReadOnly();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#setCatalog(java.lang.String)
     */
    @Override
    public void setCatalog(final String catalog) throws SQLException {
        assertOpened();
        try {
            this.physicalConnection.setCatalog(catalog);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#getCatalog()
     */
    @Override
    public String getCatalog() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.getCatalog();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#close()
     */
    @Override
    public void close() {
        if (this.closed) {
            return;
        }
        LOGGER.debug(this.transaction);
        if (this.transaction == null) {
            this.connectionPool.checkIn(this);
        } else {
            this.connectionPool.checkInTx(this.transaction);
        }
    }

    /**
     * @see java.sql.Connection#setTransactionIsolation(int)
     */
    @Override
    public void setTransactionIsolation(final int level) throws SQLException {
        assertOpened();
        try {
            this.physicalConnection.setTransactionIsolation(level);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#getTransactionIsolation()
     */
    @Override
    public int getTransactionIsolation() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.getTransactionIsolation();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#getWarnings()
     */
    @Override
    public SQLWarning getWarnings() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.getWarnings();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#clearWarnings()
     */
    @Override
    public void clearWarnings() throws SQLException {
        assertOpened();
        try {
            this.physicalConnection.clearWarnings();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#commit()
     */
    @Override
    public void commit() throws SQLException {
        assertOpened();
        assertLocalTx();
        try {
            this.physicalConnection.commit();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#rollback()
     */
    @Override
    public void rollback() throws SQLException {
        assertOpened();
        assertLocalTx();
        try {
            this.physicalConnection.rollback();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#setAutoCommit(boolean)
     */
    @Override
    public void setAutoCommit(final boolean autoCommit) throws SQLException {
        assertOpened();
        if (autoCommit) {
            assertLocalTx();
        }
        try {
            this.physicalConnection.setAutoCommit(autoCommit);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#getAutoCommit()
     */
    @Override
    public boolean getAutoCommit() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.getAutoCommit();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#createStatement(int, int)
     */
    @Override
    public Statement createStatement(final int resultSetType,
            final int resultSetConcurrency) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.createStatement(resultSetType,
                    resultSetConcurrency);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#getTypeMap()
     */
    @Override
    public Map<String, Class<?>> getTypeMap() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.getTypeMap();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#setTypeMap(java.util.Map)
     */
    @Override
    public void setTypeMap(final Map<String, Class<?>> map) throws SQLException {
        assertOpened();
        try {
            this.physicalConnection.setTypeMap(map);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#prepareStatement(java.lang.String, int, int)
     */
    @Override
    public PreparedStatement prepareStatement(final String sql, final int resultSetType,
            final int resultSetConcurrency) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.prepareStatement(
                    sql, resultSetType, resultSetConcurrency);
        } catch (final SQLException ex) {
            release();
            LOGGER.info(sql);
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#prepareCall(java.lang.String, int, int)
     */
    @Override
    public CallableStatement prepareCall(final String sql, final int resultSetType,
            final int resultSetConcurrency) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.prepareCall(
                    sql, resultSetType, resultSetConcurrency);
        } catch (final SQLException ex) {
            release();
            LOGGER.info(sql);
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#setHoldability(int)
     */
    @Override
    public void setHoldability(final int holdability) throws SQLException {
        assertOpened();
        try {
            this.physicalConnection.setHoldability(holdability);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#getHoldability()
     */
    @Override
    public int getHoldability() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.getHoldability();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#setSavepoint()
     */
    @Override
    public Savepoint setSavepoint() throws SQLException {
        assertOpened();
        assertLocalTx();
        try {
            return this.physicalConnection.setSavepoint();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#setSavepoint(java.lang.String)
     */
    @Override
    public Savepoint setSavepoint(final String name) throws SQLException {
        assertOpened();
        assertLocalTx();
        try {
            return this.physicalConnection.setSavepoint(name);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#rollback(java.sql.Savepoint)
     */
    @Override
    public void rollback(final Savepoint savepoint) throws SQLException {
        assertOpened();
        assertLocalTx();
        try {
            this.physicalConnection.rollback(savepoint);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#releaseSavepoint(java.sql.Savepoint)
     */
    @Override
    public void releaseSavepoint(final Savepoint savepoint) throws SQLException {
        assertOpened();
        assertLocalTx();
        try {
            this.physicalConnection.releaseSavepoint(savepoint);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#createStatement(int, int, int)
     */
    @Override
    public Statement createStatement(final int resultSetType, final int resultSetConcurrency,
            final int resultSetHoldability) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.createStatement(resultSetType,
                    resultSetConcurrency, resultSetHoldability);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#prepareStatement(java.lang.String, int, int, int)
     */
    @Override
    public PreparedStatement prepareStatement(final String sql,
            final int resultSetType, final int resultSetConcurrency,
            final int resultSetHoldability) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.prepareStatement(
                    sql, resultSetType, resultSetConcurrency, resultSetHoldability);
        } catch (final SQLException ex) {
            release();
            LOGGER.info(sql);
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#prepareCall(java.lang.String, int, int, int)
     */
    @Override
    public CallableStatement prepareCall(final String sql,
            final int resultSetType, final int resultSetConcurrency,
            final int resultSetHoldability) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.prepareCall(
                    sql, resultSetType, resultSetConcurrency, resultSetHoldability);
        } catch (final SQLException ex) {
            release();
            LOGGER.info(sql);
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#prepareStatement(java.lang.String, int)
     */
    @Override
    public PreparedStatement prepareStatement(final String sql,
            final int autoGeneratedKeys) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.prepareStatement(sql, autoGeneratedKeys);
        } catch (final SQLException ex) {
            release();
            LOGGER.info(sql);
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#prepareStatement(java.lang.String, int[])
     */
    @Override
    public PreparedStatement prepareStatement(final String sql,
            final int[] columnIndexes) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.prepareStatement(sql, columnIndexes);
        } catch (final SQLException ex) {
            release();
            LOGGER.info(sql);
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#prepareStatement(java.lang.String, java.lang.String[])
     */
    @Override
    public PreparedStatement prepareStatement(final String sql,
            final String[] columnNames) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.prepareStatement(sql, columnNames);
        } catch (final SQLException ex) {
            release();
            LOGGER.info(sql);
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#createClob()
     */
    @Override
    public Clob createClob() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.createClob();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#createBlob()
     */
    @Override
    public Blob createBlob() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.createBlob();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#createNClob()
     */
    @Override
    public NClob createNClob() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.createNClob();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#createSQLXML()
     */
    @Override
    public SQLXML createSQLXML() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.createSQLXML();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#isValid(int)
     */
    @Override
    public boolean isValid(final int timeout) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.isValid(timeout);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#setClientInfo(java.lang.String, java.lang.String)
     */
    @Override
    public void setClientInfo(final String name, final String value) throws SQLClientInfoException {
        this.physicalConnection.setClientInfo(name, value);
    }

    /**
     * @see java.sql.Connection#setClientInfo(java.util.Properties)
     */
    @Override
    public void setClientInfo(final Properties properties) throws SQLClientInfoException {
        this.physicalConnection.setClientInfo(properties);
    }

    /**
     * @see java.sql.Connection#getClientInfo(java.lang.String)
     */
    @Override
    public String getClientInfo(final String name) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.getClientInfo(name);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#getClientInfo()
     */
    @Override
    public Properties getClientInfo() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.getClientInfo();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#createArrayOf(java.lang.String, java.lang.Object[])
     */
    @Override
    public Array createArrayOf(final String typeName, final Object[] elements) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.createArrayOf(typeName, elements);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#createStruct(java.lang.String, java.lang.Object[])
     */
    @Override
    public Struct createStruct(final String typeName,
            final Object[] attributes) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.createStruct(typeName, attributes);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#setSchema(java.lang.String)
     */
    @Override
    public void setSchema(final String schema) throws SQLException {
        assertOpened();
        try {
            this.physicalConnection.setSchema(schema);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#getSchema()
     */
    @Override
    public String getSchema() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.getSchema();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#abort(java.util.concurrent.Executor)
     */
    @Override
    public void abort(final Executor executor) throws SQLException {
        assertOpened();
        try {
            this.physicalConnection.abort(executor);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#setNetworkTimeout(java.util.concurrent.Executor, int)
     */
    @Override
    public void setNetworkTimeout(final Executor executor,
            final int milliseconds) throws SQLException {
        assertOpened();
        try {
            this.physicalConnection.setNetworkTimeout(executor, milliseconds);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Connection#getNetworkTimeout()
     */
    @Override
    public int getNetworkTimeout() throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.getNetworkTimeout();
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Wrapper#unwrap(java.lang.Class)
     */
    @Override
    public <T> T unwrap(final Class<T> iface) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.unwrap(iface);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }

    /**
     * @see java.sql.Wrapper#isWrapperFor(java.lang.Class)
     */
    @Override
    public boolean isWrapperFor(final Class<?> iface) throws SQLException {
        assertOpened();
        try {
            return this.physicalConnection.isWrapperFor(iface);
        } catch (final SQLException ex) {
            release();
            throw ex;
        }
    }
}
