/*
 * This software is distributed under following license based on modified BSD
 * style license.
 * ----------------------------------------------------------------------
 * 
 * Copyright 2009 The Nimbus2 Project. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE NIMBUS PROJECT ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 * NO EVENT SHALL THE NIMBUS PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are
 * those of the authors and should not be interpreted as representing official
 * policies, either expressed or implied, of the Nimbus2 Project.
 */
package jp.ossc.nimbus.sql;

import java.io.Serializable;
import java.sql.*;
import java.sql.Array;
import java.util.*;
import java.util.concurrent.Executor;
import java.lang.reflect.*;

import jp.ossc.nimbus.beans.*;

/**
 * Connectionラッパー。<p>
 * 
 * @author M.Takata
 */
public class ConnectionWrapper implements Connection, Serializable {
    
    private static final long serialVersionUID = 6168176679889807870L;
    
    protected Connection connection;
    
    protected Class<? extends Statement> statementWrapperClass;
    
    protected Class<? extends CallableStatement> callableStatementWrapperClass;
    
    protected Class<? extends PreparedStatement> preparedStatementWrapperClass;
    
    protected Class<? extends ResultSet> resultSetWrapperClass;
    
    protected Map<Property, Object> statementProperties;
    
    protected Map<Property, Object> callableStatementProperties;
    
    protected Map<Property, Object> preparedStatementProperties;
    
    protected Map<String, Object> resultSetProperties;
    
    /**
     * 指定したコネクションをラップするインスタンスを生成する。<p>
     *
     * @param con ラップするコネクション
     */
    public ConnectionWrapper(Connection con){
        connection = con;
    }
    
    /**
     * ラップしているコネクションを取得する。<p>
     *
     * @return ラップしているコネクション
     */
    public Connection getConnection(){
        return connection;
    }
    
    /**
     * ラップするコネクションを設定する。<p>
     *
     * @param con ラップするコネクション
     */
    public void setConnection(Connection con){
        connection = con;
    }
    
    /**
     * ラップする{@link StatementWrapper}の実装クラスを設定する。<p>
     *
     * @param clazz ラップするStatementWrapperの実装クラス
     * @exception IllegalArgumentException 指定したクラスがStatementのサブクラスでない場合
     */
    public void setStatementWrapperClass(Class<? extends Statement> clazz)
     throws IllegalArgumentException{
        
        if(clazz != null && !Statement.class.isAssignableFrom(clazz)){
            throw new IllegalArgumentException(
                "Illegal class : " + clazz.getName()
            );
        }
        statementWrapperClass = clazz;
    }
    
    /**
     * ラップする{@link StatementWrapper}の実装クラスを取得する。<p>
     *
     * @return ラップするStatementWrapperの実装クラス
     */
    public Class<? extends Statement> getStatementWrapperClass(){
        return statementWrapperClass;
    }
    
    /**
     * ラップする{@link CallableStatement}の実装クラスを設定する。<p>
     *
     * @param clazz ラップするCallableStatementの実装クラス
     * @exception IllegalArgumentException 指定したクラスがCallableStatementの実装クラスでない場合
     */
    public void setCallableStatementWrapperClass(Class<? extends CallableStatement> clazz)
     throws IllegalArgumentException{
        
        if(clazz != null
             && !CallableStatement.class.isAssignableFrom(clazz)){
            throw new IllegalArgumentException(
                "Illegal class : " + clazz.getName()
            );
        }
        callableStatementWrapperClass = clazz;
    }
    
    /**
     * ラップする{@link CallableStatementWrapper}の実装クラスを取得する。<p>
     *
     * @return ラップするCallableStatementWrapperの実装クラス
     */
    public Class<? extends CallableStatement> getCallableStatementWrapperClass(){
        return callableStatementWrapperClass;
    }
    
    /**
     * ラップする{@link PreparedStatement}の実装クラスを設定する。<p>
     *
     * @param clazz ラップするPreparedStatementの実装クラス
     * @exception IllegalArgumentException 指定したクラスがPreparedStatementの実装クラスでない場合
     */
    public void setPreparedStatementWrapperClass(Class<? extends PreparedStatement> clazz)
     throws IllegalArgumentException{
        
        if(clazz != null
             && !PreparedStatement.class.isAssignableFrom(clazz)){
            throw new IllegalArgumentException(
                "Illegal class : " + clazz.getName()
            );
        }
        preparedStatementWrapperClass = clazz;
    }
    
    /**
     * ラップする{@link PreparedStatementWrapper}の実装クラスを取得する。<p>
     *
     * @return ラップするPreparedStatementWrapperの実装クラス
     */
    public Class<? extends PreparedStatement> getPreparedStatementWrapperClass(){
        return preparedStatementWrapperClass;
    }
    
    /**
     * ラップする{@link ResultSetWrapper}の実装クラスを設定する。<p>
     *
     * @param clazz ラップするResultSetWrapperの実装クラス
     * @exception IllegalArgumentException 指定したクラスがResultSetのサブクラスでない場合
     */
    public void setResultSetWrapperClass(Class<? extends ResultSet> clazz)
     throws IllegalArgumentException{
        
        if(clazz != null
             && !ResultSet.class.isAssignableFrom(clazz)){
            throw new IllegalArgumentException(
                "Illegal class : " + clazz.getName()
            );
        }
        resultSetWrapperClass = clazz;
    }
    
    /**
     * ラップする{@link ResultSetWrapper}の実装クラスを取得する。<p>
     *
     * @return ラップするResultSetWrapperの実装クラス
     */
    public Class<? extends ResultSet> getResultSetWrapperClass(){
        return resultSetWrapperClass;
    }
    
    /**
     * 全てのStatementにプロパティを設定する。<p>
     * {@link #setStatementProperties(Map)}、{@link #setCallableStatementProperties(Map)}、{@link #setPreparedStatementProperties(Map)}を呼び出す。<br>
     *
     * @param props プロパティマップ
     */
    public void setAllStatementProperties(Map<String, Object> props){
        setStatementProperties(props);
        setCallableStatementProperties(props);
        setPreparedStatementProperties(props);
    }
    
    /**
     * 全てのStatementにプロパティを設定する。<p>
     * {@link #setStatementProperty(String, Object)}、{@link #setCallableStatementProperty(String, Object)}、{@link #setPreparedStatementProperty(String, Object)}を呼び出す。<br>
     *
     * @param name プロパティ名
     * @param value 値
     */
    public void setAllStatementProperty(String name, Object value){
        setStatementProperty(name, value);
        setCallableStatementProperty(name, value);
        setPreparedStatementProperty(name, value);
    }
    
    /**
     * {@link StatementWrapper}にプロパティを設定する。<p>
     *
     * @param props プロパティマップ
     */
    public void setStatementProperties(Map<String, Object> props){
        if(props == null || props.size() == 0){
            if(statementProperties != null){
                statementProperties = null;
            }
            return;
        }
        for(Map.Entry<String, Object> entry : props.entrySet()){
            setStatementProperty(entry.getKey(), entry.getValue());
        }
    }
    
    /**
     * {@link StatementWrapper}にプロパティを設定する。<p>
     *
     * @param name プロパティ名
     * @param value 値
     */
    public void setStatementProperty(String name, Object value){
        if(statementProperties == null){
            statementProperties = new LinkedHashMap<Property, Object>();
        }
        final Property prop = PropertyFactory.createProperty(name);
        statementProperties.put(prop, value);
    }
    
    /**
     * {@link StatementWrapper}のプロパティを取得する。<p>
     *
     * @param name プロパティ名
     * @return 値
     */
    public Object getStatementProperty(String name){
        if(statementProperties == null){
            return null;
        }
        for(Map.Entry<Property, Object> entry : statementProperties.entrySet()){
            if(entry.getKey().getPropertyName().equals(name)){
                return entry.getValue();
            }
        }
        return null;
    }
    
    /**
     * {@link CallableStatementWrapper}にプロパティを設定する。<p>
     *
     * @param props プロパティマップ
     */
    public void setCallableStatementProperties(Map<String, Object> props){
        if(props == null || props.size() == 0){
            if(callableStatementProperties != null){
                callableStatementProperties = null;
            }
            return;
        }
        for(Map.Entry<String, Object> entry : props.entrySet()){
            setCallableStatementProperty(entry.getKey(), entry.getValue());
        }
    }
    
    /**
     * {@link CallableStatementWrapper}にプロパティを設定する。<p>
     *
     * @param name プロパティ名
     * @param value 値
     */
    public void setCallableStatementProperty(String name, Object value){
        if(callableStatementProperties == null){
            callableStatementProperties = new LinkedHashMap<Property, Object>();
        }
        final Property prop = PropertyFactory.createProperty(name);
        callableStatementProperties.put(prop, value);
    }
    
    /**
     * {@link CallableStatementWrapper}のプロパティを取得する。<p>
     *
     * @param name プロパティ名
     * @return 値
     */
    public Object getCallableStatementProperty(String name){
        if(callableStatementProperties == null){
            return null;
        }
        for(Map.Entry<Property, Object> entry : callableStatementProperties.entrySet()){
            if(entry.getKey().getPropertyName().equals(name)){
                return entry.getValue();
            }
        }
        return null;
    }
    
    /**
     * {@link PreparedStatementWrapper}にプロパティを設定する。<p>
     *
     * @param props プロパティマップ
     */
    public void setPreparedStatementProperties(Map<String, Object> props){
        if(props == null || props.size() == 0){
            if(preparedStatementProperties != null){
                preparedStatementProperties = null;
            }
            return;
        }
        for(Map.Entry<String, Object> entry : props.entrySet()){
            setPreparedStatementProperty(entry.getKey(), entry.getValue());
        }
    }
    
    /**
     * {@link PreparedStatementWrapper}にプロパティを設定する。<p>
     *
     * @param name プロパティ名
     * @param value 値
     */
    public void setPreparedStatementProperty(String name, Object value){
        if(preparedStatementProperties == null){
            preparedStatementProperties = new LinkedHashMap<Property, Object>();
        }
        final Property prop = PropertyFactory.createProperty(name);
        preparedStatementProperties.put(prop, value);
    }
    
    /**
     * {@link PreparedStatementWrapper}のプロパティを取得する。<p>
     *
     * @param name プロパティ名
     * @return 値
     */
    public Object getPreparedStatementProperty(String name){
        if(preparedStatementProperties == null){
            return null;
        }
        for(Map.Entry<Property, Object> entry : preparedStatementProperties.entrySet()){
            if(entry.getKey().getPropertyName().equals(name)){
                return entry.getValue();
            }
        }
        return null;
    }
    
    /**
     * {@link ResultSetWrapper}にプロパティを設定する。<p>
     *
     * @param props プロパティマップ
     */
    public void setResultSetProperties(Map<String, Object> props){
        if(props == null || props.size() == 0){
            if(resultSetProperties != null){
                resultSetProperties = null;
            }
            return;
        }
        for(Map.Entry<String, Object> entry : props.entrySet()){
            setResultSetProperty(entry.getKey(), entry.getValue());
        }
    }
    
    /**
     * {@link ResultSetWrapper}にプロパティを設定する。<p>
     *
     * @param name プロパティ名
     * @param value 値
     */
    public void setResultSetProperty(String name, Object value){
        if(resultSetProperties == null){
            resultSetProperties = new LinkedHashMap<String, Object>();
        }
        resultSetProperties.put(name, value);
    }
    
    /**
     * {@link ResultSetWrapper}のプロパティを取得する。<p>
     *
     * @param name プロパティ名
     * @return 値
     */
    public Object getResultSetProperty(String name){
        if(resultSetProperties == null){
            return null;
        }
        return resultSetProperties.get(name);
    }
    
    protected Statement createStatementWrapper(Statement stmt)
     throws SQLException{
        if(statementWrapperClass == null){
            return stmt;
        }
        Statement result = null;
        try{
            final Constructor<? extends Statement> constructor
                 = statementWrapperClass.getConstructor(
                    new Class[]{
                        Connection.class,
                        Statement.class
                    }
                );
            result = constructor.newInstance(
                new Object[]{this, stmt}
            );
            if(result instanceof StatementWrapper){
                applyStatementProperties((StatementWrapper)result);
            }
        }catch(InvocationTargetException e){
            throw new SQLException(e.getTargetException().toString());
        }catch(Exception e){
            throw new SQLException(e.toString());
        }
        return result;
    }
    
    protected void applyStatementProperties(StatementWrapper stmtw)
     throws Exception{
        applyStatementProperties(stmtw, statementProperties);
    }
    
    protected CallableStatement createCallableStatementWrapper(
        CallableStatement stmt,
        String sql
    ) throws SQLException{
        if(callableStatementWrapperClass == null){
            return stmt;
        }
        CallableStatement result = null;
        try{
            final Constructor<? extends CallableStatement> constructor
                 = callableStatementWrapperClass.getConstructor(
                    new Class[]{
                        Connection.class,
                        CallableStatement.class,
                        String.class
                    }
                );
            result = constructor.newInstance(
                new Object[]{this, stmt, sql}
            );
            if(result instanceof StatementWrapper){
                applyCallableStatementProperties((StatementWrapper)result);
            }
        }catch(InvocationTargetException e){
            throw new SQLException(e.getTargetException().toString());
        }catch(Exception e){
            throw new SQLException(e.toString());
        }
        return (CallableStatement)result;
    }
    
    protected void applyCallableStatementProperties(StatementWrapper stmtw)
     throws Exception{
        applyStatementProperties(stmtw, callableStatementProperties);
    }
    
    protected PreparedStatement createPreparedStatementWrapper(
        PreparedStatement stmt,
        String sql
    ) throws SQLException{
        if(preparedStatementWrapperClass == null){
            return stmt;
        }
        PreparedStatement result = null;
        try{
            final Constructor<? extends PreparedStatement> constructor
                 = preparedStatementWrapperClass.getConstructor(
                    new Class[]{
                        Connection.class,
                        PreparedStatement.class,
                        String.class
                    }
                );
            result = constructor.newInstance(
                new Object[]{this, stmt, sql}
            );
            if(result instanceof StatementWrapper){
                applyPreparedStatementProperties((StatementWrapper)result);
            }
        }catch(InvocationTargetException e){
            throw new SQLException(e.getTargetException().toString());
        }catch(Exception e){
            throw new SQLException(e.toString());
        }
        return result;
    }
    
    protected void applyPreparedStatementProperties(StatementWrapper stmtw)
     throws Exception{
        applyStatementProperties(stmtw, preparedStatementProperties);
    }
    
    protected void applyStatementProperties(
        StatementWrapper stmt,
        Map<Property, Object> properties
    ) throws Exception{
        if(resultSetWrapperClass != null){
            stmt.setResultSetWrapperClass(resultSetWrapperClass);
            if(resultSetProperties != null && resultSetProperties.size() != 0){
                for(Map.Entry<String, Object> entry : resultSetProperties.entrySet()){
                    stmt.setResultSetProperty(
                        entry.getKey(),
                        entry.getValue()
                    );
                }
            }
        }
        if(properties != null && properties.size() != 0){
            for(Map.Entry<Property, Object> entry : properties.entrySet()){
                entry.getKey().setProperty(stmt, entry.getValue());
            }
        }
    }
    
    @Override
    public int getHoldability() throws SQLException {
        return connection.getHoldability();
    }
    
    @Override
    public int getTransactionIsolation() throws SQLException {
        return connection.getTransactionIsolation();
    }
    
    @Override
    public void clearWarnings() throws SQLException {
        connection.clearWarnings();
    }
    
    @Override
    public void close() throws SQLException {
        connection.close();
    }
    
    @Override
    public void commit() throws SQLException {
        connection.commit();
    }
    
    @Override
    public void rollback() throws SQLException {
        connection.rollback();
    }
    
    @Override
    public boolean getAutoCommit() throws SQLException {
        return connection.getAutoCommit();
    }
    
    @Override
    public boolean isClosed() throws SQLException {
        return connection.isClosed();
    }
    
    @Override
    public boolean isReadOnly() throws SQLException {
        return connection.isReadOnly();
    }
    
    @Override
    public void setHoldability(int arg0) throws SQLException {
        connection.setHoldability(arg0);
    }
    
    @Override
    public void setTransactionIsolation(int arg0) throws SQLException {
        connection.setTransactionIsolation(arg0);
    }
    
    @Override
    public void setAutoCommit(boolean arg0) throws SQLException {
        connection.setAutoCommit(arg0);
    }
    
    @Override
    public void setReadOnly(boolean arg0) throws SQLException {
        connection.setReadOnly(arg0);
    }
    
    @Override
    public String getCatalog() throws SQLException {
        return connection.getCatalog();
    }
    
    @Override
    public void setCatalog(String arg0) throws SQLException {
        connection.setCatalog(arg0);
    }
    
    @Override
    public DatabaseMetaData getMetaData() throws SQLException {
        return connection.getMetaData();
    }
    
    @Override
    public SQLWarning getWarnings() throws SQLException {
        return connection.getWarnings();
    }
    
    @Override
    public Savepoint setSavepoint() throws SQLException {
        return connection.setSavepoint();
    }
    
    @Override
    public void releaseSavepoint(Savepoint arg0) throws SQLException {
        connection.releaseSavepoint(arg0);
    }
    
    @Override
    public void rollback(Savepoint arg0) throws SQLException {
        connection.rollback(arg0);
    }
    
    @Override
    public Statement createStatement() throws SQLException {
        return createStatementWrapper(connection.createStatement());
    }
    
    @Override
    public Statement createStatement(int arg0, int arg1) throws SQLException {
        return createStatementWrapper(connection.createStatement(arg0, arg1));
    }
    
    @Override
    public Statement createStatement(int arg0, int arg1, int arg2)
     throws SQLException {
        return createStatementWrapper(
            connection.createStatement(arg0, arg1, arg2)
        );
    }
    
    @Override
    public Map<String, Class<?>> getTypeMap() throws SQLException {
        return connection.getTypeMap();
    }
    
    @Override
    public void setTypeMap(Map<String, Class<?>> arg0) throws SQLException {
        connection.setTypeMap(arg0);
    }
    
    @Override
    public String nativeSQL(String arg0) throws SQLException {
        return connection.nativeSQL(arg0);
    }
    
    @Override
    public CallableStatement prepareCall(String arg0) throws SQLException {
        return createCallableStatementWrapper(
            connection.prepareCall(arg0),
            arg0
        );
    }
    
    @Override
    public CallableStatement prepareCall(String arg0, int arg1, int arg2)
            throws SQLException {
        return createCallableStatementWrapper(
            connection.prepareCall(arg0, arg1, arg2),
            arg0
        );
    }
    
    @Override
    public CallableStatement prepareCall(String arg0, int arg1, int arg2,
            int arg3) throws SQLException {
        return createCallableStatementWrapper(
            connection.prepareCall(arg0, arg1, arg2, arg3),
            arg0
        );
    }
    
    @Override
    public PreparedStatement prepareStatement(String arg0) throws SQLException {
        return createPreparedStatementWrapper(
            connection.prepareStatement(arg0),
            arg0
        );
    }
    
    @Override
    public PreparedStatement prepareStatement(String arg0, int arg1)
     throws SQLException {
        return createPreparedStatementWrapper(
            connection.prepareStatement(arg0, arg1),
            arg0
        );
    }
    
    @Override
    public PreparedStatement prepareStatement(String arg0, int arg1, int arg2)
     throws SQLException {
        return createPreparedStatementWrapper(
            connection.prepareStatement(arg0, arg1, arg2),
            arg0
        );
    }
    
    @Override
    public PreparedStatement prepareStatement(
        String arg0,
        int arg1,
        int arg2,
        int arg3
    ) throws SQLException {
        return createPreparedStatementWrapper(
            connection.prepareStatement(arg0, arg1, arg2, arg3),
            arg0
        );
    }
    
    @Override
    public PreparedStatement prepareStatement(String arg0, int[] arg1)
     throws SQLException {
        return createPreparedStatementWrapper(
            connection.prepareStatement(arg0, arg1),
            arg0
        );
    }
    
    @Override
    public PreparedStatement prepareStatement(String arg0, String[] arg1)
     throws SQLException {
        return createPreparedStatementWrapper(
            connection.prepareStatement(arg0, arg1),
            arg0
        );
    }
    
    @Override
    public Savepoint setSavepoint(String arg0) throws SQLException {
        return connection.setSavepoint(arg0);
    }
    
    @Override
    public Struct createStruct(String typeName, Object[] attributes) throws SQLException{
        return connection.createStruct(typeName, attributes);
    }
    
    @Override
    public Array createArrayOf(String typeName, Object[] elements) throws SQLException{
        return connection.createArrayOf(typeName, elements);
    }
    
    @Override
    public Properties getClientInfo() throws SQLException{
        return connection.getClientInfo();
    }
    
    @Override
    public String getClientInfo(String name) throws SQLException{
        return connection.getClientInfo(name);
    }
    
    @Override
    public void setClientInfo(Properties properties) throws SQLClientInfoException{
        connection.setClientInfo(properties);
    }
    
    @Override
    public void setClientInfo(String name, String value) throws SQLClientInfoException{
        connection.setClientInfo(name, value);
    }
    
    @Override
    public boolean isValid(int timeout) throws SQLException{
        return connection.isValid(timeout);
    }
    
    @Override
    public SQLXML createSQLXML() throws SQLException{
        return connection.createSQLXML();
    }
    
    @Override
    public Blob createBlob() throws SQLException{
        return connection.createBlob();
    }
    
    @Override
    public Clob createClob() throws SQLException{
        return connection.createClob();
    }
    
    @Override
    public NClob createNClob() throws SQLException{
        return connection.createNClob();
    }
    
    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException{
        return connection.isWrapperFor(iface);
    }
    
    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException{
        return connection.unwrap(iface);
    }

    @Override
    public void setSchema(String schema) throws SQLException {
        connection.setSchema(schema);
    }

    @Override
    public String getSchema() throws SQLException {
        return connection.getSchema();
    }

    @Override
    public void abort(Executor executor) throws SQLException {
        connection.abort(executor);
    }

    @Override
    public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
        connection.setNetworkTimeout(executor, milliseconds);
    }

    @Override
    public int getNetworkTimeout() throws SQLException {
        return connection.getNetworkTimeout();
    }
    
    @Override
    protected void finalize() throws Throwable{
        try{
            if(!isClosed()){
                close();
            }
        }catch(SQLException e){
        }
        super.finalize();
    }
}
