/*
 * 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.openjpa.lib.jdbc;

import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.Array;
import java.sql.BatchUpdateException;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;

import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.J2DoPrivHelper;

/**
 * A {@link ConnectionDecorator} that creates logging connections and
 * {@link ReportingSQLException}s.
 *
 * @author Marc Prud'hommeaux
 */
public class LoggingConnectionDecorator implements ConnectionDecorator {

    private static final String SEP = J2DoPrivHelper.getLineSeparator();

    private static final int WARN_IGNORE = 0;
    private static final int WARN_LOG_TRACE = 1;
    private static final int WARN_LOG_INFO = 2;
    private static final int WARN_LOG_WARN = 3;
    private static final int WARN_LOG_ERROR = 4;
    private static final int WARN_THROW = 5;
    private static final int WARN_HANDLE = 6;
    private static final String[] WARNING_ACTIONS = new String[7];

    static {
        WARNING_ACTIONS[WARN_IGNORE] = "ignore";
        WARNING_ACTIONS[WARN_LOG_TRACE] = "trace";
        WARNING_ACTIONS[WARN_LOG_INFO] = "info";
        WARNING_ACTIONS[WARN_LOG_WARN] = "warn";
        WARNING_ACTIONS[WARN_LOG_ERROR] = "error";
        WARNING_ACTIONS[WARN_THROW] = "throw";
        WARNING_ACTIONS[WARN_HANDLE] = "handle";
    }

    private final DataSourceLogs _logs = new DataSourceLogs();
    private SQLFormatter _formatter;
    private boolean _prettyPrint;
    private int _prettyPrintLineLength = 60;
    private int _warningAction = WARN_IGNORE;
    private SQLWarningHandler _warningHandler;
    private boolean _printParameters = false;

    /**
     * If set to <code>true</code>, pretty-print SQL by running it
     * through {@link SQLFormatter#prettyPrint}. If
     * <code>false</code>, don't pretty-print, and output SQL logs in
     * a single line. Pretty-printed SQL can be easier for a human to
     * read, but is harder to parse with tools like grep.
     */
    public void setPrettyPrint(boolean prettyPrint) {
        _prettyPrint = prettyPrint;
        if (_formatter == null && _prettyPrint) {
            _formatter = new SQLFormatter();
            _formatter.setLineLength(_prettyPrintLineLength);
        } else if (!_prettyPrint)
            _formatter = null;
    }

    /**
     * @see #setPrettyPrint
     */
    public boolean getPrettyPrint() {
        return _prettyPrint;
    }

    /**
     * The number of characters to print per line when
     * pretty-printing of SQL is enabled. Defaults to 60 to provide
     * some space for any ant-related characters on the left of a
     * standard 80-character display.
     */
    public void setPrettyPrintLineLength(int length) {
        _prettyPrintLineLength = length;
        if (_formatter != null)
            _formatter.setLineLength(length);
    }

    /**
     * @see #setPrettyPrintLineLength
     */
    public int getPrettyPrintLineLength() {
        return _prettyPrintLineLength;
    }

    /**
     * <p>
     * Whether parameter values will be printed in exception messages or in trace. This is different from
     * trackParameters which controls whether OpenJPA will track parameters internally (visible while debugging and used
     * in batching).
     * </p>
     */
    public boolean getPrintParameters() {
        return _printParameters;
    }

    public void setPrintParameters(boolean printParameters) {
        _printParameters = printParameters;
    }

    /**
     * What to do with SQL warnings.
     */
    public void setWarningAction(String warningAction) {
        int index = Arrays.asList(WARNING_ACTIONS).indexOf(warningAction);
        if (index < 0)
            index = WARN_IGNORE;
        _warningAction = index;
    }

    /**
     * What to do with SQL warnings.
     */
    public String getWarningAction() {
        return WARNING_ACTIONS[_warningAction];
    }

    /**
     * What to do with SQL warnings.
     */
    public void setWarningHandler(SQLWarningHandler warningHandler) {
        _warningHandler = warningHandler;
    }

    /**
     * What to do with SQL warnings.
     */
    public SQLWarningHandler getWarningHandler() {
        return _warningHandler;
    }

    /**
     * The log to write to.
     */
    public DataSourceLogs getLogs() {
        return _logs;
    }

    @Override
    public Connection decorate(Connection conn) throws SQLException {
        return newLoggingConnection(conn);
    }

    private LoggingConnection newLoggingConnection(Connection conn)
        throws SQLException {
        return new LoggingConnection(conn);
    }

    private SQLException wrap(SQLException sqle, Statement stmnt) {
        return wrap(sqle, stmnt, null, -1);
    }

    private SQLException wrap(SQLException sqle, String sql) {
        return wrap(sqle, null, sql, -1);
    }

    private SQLException wrap(SQLException sqle, Statement stmnt, String sql) {
        return wrap(sqle, stmnt, sql, -1);
    }

    private SQLException wrap(SQLException sqle, Statement stmnt, int indexOfFailedBatchObject) {
        return wrap(sqle, stmnt, null, indexOfFailedBatchObject);
    }

    /**
     * Include SQL in exception.
     */
    private SQLException wrap(SQLException sqle, Statement stmnt, String sql, int indexOfFailedBatchObject) {
        ReportingSQLException toReturn = null;

        if (sqle instanceof ReportingSQLException) {
            toReturn = (ReportingSQLException) sqle;
        } else {
            toReturn = new ReportingSQLException(sqle, stmnt, sql);
        }

        toReturn.setIndexOfFirstFailedObject(indexOfFailedBatchObject);
        return toReturn;
    }

    /**
     * Interface that allows customization of what to do when
     * {@link SQLWarning}s occur.
     */
    public interface SQLWarningHandler {

        void handleWarning(SQLWarning warning) throws SQLException;
    }

    /**
     * Logging connection.
     */
    protected class LoggingConnection extends DelegatingConnection {

        public LoggingConnection(Connection conn) throws SQLException {
            super(conn);
        }

        @Override
        protected PreparedStatement prepareStatement(String sql, boolean wrap)
            throws SQLException {
            SQLException err = null;
            try {
                PreparedStatement stmnt = super.prepareStatement(sql, false);
                return newLoggingPreparedStatement(stmnt, sql);
            } catch (SQLException se) {
                err = wrap(se, sql);
                throw err;
            }  finally {
                handleSQLErrors(err);
            }
        }

        @Override
        protected PreparedStatement prepareStatement(String sql, int rsType,
            int rsConcur, boolean wrap) throws SQLException {
            SQLException err = null;
            try {
                PreparedStatement stmnt = super.prepareStatement
                    (sql, rsType, rsConcur, false);
                return newLoggingPreparedStatement(stmnt, sql);
            } catch (SQLException se) {
                err =  wrap(se, sql);
                throw err;
            } finally {
                handleSQLErrors(err);
            }
        }

        @Override
        protected Statement createStatement(boolean wrap) throws SQLException {
            SQLException err = null;
            try {
                Statement stmnt = super.createStatement(false);
                return newLoggingStatement(stmnt);
            }catch (SQLException se) {
                err = se;
                throw se;
            } finally {
                handleSQLErrors(err);
            }
        }

        @Override
        protected Statement createStatement(int type, int concurrency,
            boolean wrap) throws SQLException {
            SQLException err = null;
            try {
                Statement stmnt = super.createStatement(type, concurrency,
                    false);
                return newLoggingStatement(stmnt);
            } catch (SQLException se) {
                err = se;
                throw se;
            } finally {
                handleSQLErrors(err);
            }
        }

        @Override
        protected CallableStatement prepareCall(String sql, boolean wrap)
            throws SQLException {
            SQLException err = null;
            try {
                CallableStatement stmt = super.prepareCall(sql, wrap);
                return newLoggingCallableStatement(stmt, sql);
            } catch (SQLException se) {
                err = wrap(se, sql);
                throw err;
            } finally {
                handleSQLErrors(err);
            }
        }

        private LoggingPreparedStatement newLoggingPreparedStatement
            (PreparedStatement stmnt, String sql) throws SQLException {
            return new LoggingPreparedStatement(stmnt, sql);
        }

        private CallableStatement newLoggingCallableStatement(
            CallableStatement stmnt, String sql) throws SQLException {
            return new LoggingCallableStatement(stmnt, sql);
        }

        private LoggingStatement newLoggingStatement(Statement stmnt)
            throws SQLException {
            return new LoggingStatement(stmnt);
        }

        private LoggingDatabaseMetaData newLoggingDatabaseMetaData
            (DatabaseMetaData meta) throws SQLException {
            return new LoggingDatabaseMetaData(meta);
        }



        @Override
        public void commit() throws SQLException {
            long start = System.currentTimeMillis();
            SQLException err = null;
            try {
                super.commit();
            } catch (SQLException se) {
                err = se;
                throw se;
            } finally {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("commit", start, this);
                handleSQLErrors(err);
            }
        }

        @Override
        public void rollback() throws SQLException {
            long start = System.currentTimeMillis();
            SQLException err = null;
            try {
                super.rollback();
            } catch (SQLException se) {
                err = se;
                throw se;
            } finally {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("rollback", start, this);
                handleSQLErrors(err);
            }
        }

        @Override
        public void close() throws SQLException {
            long start = System.currentTimeMillis();
            try {
                super.close();
            } finally {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("close", start, this);
            }
        }

        @Override
        public Savepoint setSavepoint() throws SQLException {
            long start = System.currentTimeMillis();
            SQLException err = null;
            try {
                return super.setSavepoint();
            } catch (SQLException se) {
                err = se;
                throw se;
            } finally {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("savepoint", start, this);
                handleSQLErrors(err);
            }
        }

        @Override
        public Savepoint setSavepoint(String name) throws SQLException {
            long start = System.currentTimeMillis();
            SQLException err = null;
            try {
                return super.setSavepoint(name);
            } catch (SQLException se) {
                err = se;
                throw se;
            } finally {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("savepoint: " + name, start, this);
                handleSQLErrors(err);
            }
        }

        @Override
        public void rollback(Savepoint savepoint) throws SQLException {
            long start = System.currentTimeMillis();
            SQLException err = null;
            try {
                super.rollback(savepoint);
            } catch (SQLException se) {
                err = se;
                throw se;
            } finally {
                if (_logs.isJDBCEnabled()) {
                    String name = null;
                    try {
                        name = savepoint.getSavepointName();
                    } catch (SQLException sqe) {
                        name = String.valueOf(savepoint.getSavepointId());
                    }
                    _logs.logJDBC("rollback: " + name, start, this);
                }
                handleSQLErrors(err);
            }
        }

        @Override
        public void releaseSavepoint(Savepoint savepoint) throws SQLException {
            long start = System.currentTimeMillis();
            SQLException err = null;
            try {
                super.releaseSavepoint(savepoint);
            } catch (SQLException se) {
                err = se;
                throw se;
            } finally {
                if (_logs.isJDBCEnabled()) {
                    String name = null;
                    try {
                        name = savepoint.getSavepointName();
                    } catch (SQLException sqe) {
                        name = String.valueOf(savepoint.getSavepointId());
                    }
                    _logs.logJDBC("release: " + name, start, this);
                }
                handleSQLErrors(err);
            }
        }

        @Override
        protected Statement createStatement(int resultSetType,
            int resultSetConcurrency, int resultSetHoldability, boolean wrap)
            throws SQLException {
            SQLException err = null;
            try {
                Statement stmnt = super.createStatement(resultSetType,
                    resultSetConcurrency, resultSetHoldability, false);
                return newLoggingStatement(stmnt);
            }catch (SQLException se) {
                err = se;
                throw se;
            } finally {
                handleSQLErrors(err);
            }
        }

        @Override
        protected PreparedStatement prepareStatement(String sql,
            int resultSetType, int resultSetConcurrency,
            int resultSetHoldability, boolean wrap) throws SQLException {
            SQLException err = null;
            try {
                PreparedStatement stmnt = super.prepareStatement
                    (sql, resultSetType, resultSetConcurrency,
                        resultSetHoldability, false);
                return newLoggingPreparedStatement(stmnt, sql);
            } catch (SQLException se) {
                err = wrap(se, sql);
                throw err;
            } finally {
                handleSQLErrors(err);
            }
        }

        @Override
        protected PreparedStatement prepareStatement(String sql,
            int autoGeneratedKeys, boolean wrap) throws SQLException {
            SQLException err = null;
            try {
                PreparedStatement stmnt = super.prepareStatement
                    (sql, autoGeneratedKeys, false);
                return newLoggingPreparedStatement(stmnt, sql);
            } catch (SQLException se) {
                err = wrap(se, sql);
                throw err;
            } finally {
                handleSQLErrors(err);
            }
        }

        @Override
        protected PreparedStatement prepareStatement(String sql,
            int[] columnIndexes, boolean wrap) throws SQLException {
            SQLException err = null;
            try {
                PreparedStatement stmnt = super.prepareStatement
                    (sql, columnIndexes, false);
                return newLoggingPreparedStatement(stmnt, sql);
            } catch (SQLException se) {
                err = wrap(se, sql);
                throw err;
            } finally {
                handleSQLErrors(err);
            }
        }

        @Override
        protected PreparedStatement prepareStatement(String sql,
            String[] columnNames, boolean wrap) throws SQLException {
            SQLException err = null;
            try {
                PreparedStatement stmnt = super.prepareStatement
                    (sql, columnNames, false);
                return newLoggingPreparedStatement(stmnt, sql);
            } catch (SQLException se) {
                err = wrap(se, sql);
                throw err;
            } finally {
                handleSQLErrors(err);
            }
        }

        @Override
        protected DatabaseMetaData getMetaData(boolean wrap)
            throws SQLException {
            return newLoggingDatabaseMetaData(super.getMetaData(false));
        }

        /**
         * Log time elapsed since given start.
         */
        private void logTime(long startTime) throws SQLException {
            if (_logs.isSQLEnabled())
                _logs.logSQL("spent", startTime, this);
        }

        /**
         * Log time elapsed since given start.
         */
        private void logSQL(Statement stmnt) throws SQLException {
            if (_logs.isSQLEnabled())
                _logs.logSQL("executing " + stmnt, this);
        }

        /**
         * Log time elapsed since given start.
         */
        private void logBatchSQL(Statement stmnt) throws SQLException {
            if (_logs.isSQLEnabled())
                _logs.logSQL("executing batch " + stmnt, this);
        }

        /**
         * Handle any {@link SQLWarning}s on the current {@link Connection}.
         * Chain throwed SQLWarnings to SQLException.
         * @see #handleSQLWarning(SQLWarning)
         */
        private void handleSQLErrors(SQLException err) throws SQLException {
            if (_warningAction == WARN_IGNORE)
                return;

            try {
                handleSQLWarning(getWarnings());
            } catch (SQLException warning) {
                if (err != null)
                    err.setNextException(warning);
                else
                    throw warning;
            } finally {
                clearWarnings();
            }
        }

        /**
         * Handle any {@link SQLWarning}s on the specified {@link Statement}.
         * Chain throwed SQLWarnings to SQLException.
         *
         * @see #handleSQLWarning(SQLWarning)
         */
        private void handleSQLErrors(Statement stmnt, SQLException err)
            throws SQLException {
            if (_warningAction == WARN_IGNORE)
                return;

            try {
                handleSQLWarning(stmnt.getWarnings());
            } catch (SQLException warning) {
                if (err != null)
                    err.setNextException(warning);
                else
                    throw warning;
            } finally {
                stmnt.clearWarnings();
            }
        }

        /**
         * Handle any {@link SQLWarning}s on the specified {@link ResultSet}.
         * Chain throwed SQLWarnings to SQLException.
         *
         * @see #handleSQLWarning(SQLWarning)
         */
        private void handleSQLErrors(ResultSet rs, SQLException err)
            throws SQLException {
            if (_warningAction == WARN_IGNORE)
                return;

            try {
                handleSQLWarning(rs.getWarnings());
            } catch (SQLException warning){
                if (err != null)
                    err.setNextException(warning);
                else
                    throw warning;
            } finally {
                rs.clearWarnings();
            }
        }

        /**
         * Handle the specified {@link SQLWarning} depending on the
         * setting of the {@link #setWarningAction} attribute.
         *
         * @param warning the warning to handle
         */
        private void handleSQLWarning(SQLWarning warning) throws SQLException {
            if (warning == null)
                return;
            if (_warningAction == WARN_IGNORE)
                return;

            Log log = _logs.getJDBCLog();
            for (; warning != null; warning = warning.getNextWarning()) {
                switch (_warningAction) {
                    case WARN_LOG_TRACE:
                        if (log.isTraceEnabled())
                            log.trace(warning);
                        break;
                    case WARN_LOG_INFO:
                        if (log.isInfoEnabled())
                            log.info(warning);
                        break;
                    case WARN_LOG_WARN:
                        if (log.isWarnEnabled())
                            log.warn(warning);
                        break;
                    case WARN_LOG_ERROR:
                        if (log.isErrorEnabled())
                            log.error(warning);
                        break;
                    case WARN_THROW:
                        // just throw it as if it were a SQLException
                        throw warning;
                    case WARN_HANDLE:
                        if (_warningHandler != null)
                            _warningHandler.handleWarning(warning);
                        break;
                    default:
                        // ignore
                        break;
                }
            }
        }

        /**
         * Metadata wrapper that logs actions.
         */
        protected class LoggingDatabaseMetaData
            extends DelegatingDatabaseMetaData {

            public LoggingDatabaseMetaData(DatabaseMetaData meta) {
                super(meta, LoggingConnection.this);
            }

            @Override
            public ResultSet getBestRowIdentifier(String catalog,
                String schema, String table, int scope, boolean nullable)
                throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getBestRowIdentifier: "
                        + catalog + ", " + schema + ", " + table,
                        LoggingConnection.this);
                return super.getBestRowIdentifier(catalog, schema,
                    table, scope, nullable);
            }

            @Override
            public ResultSet getCatalogs() throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getCatalogs", LoggingConnection.this);
                return super.getCatalogs();
            }

            @Override
            public ResultSet getColumnPrivileges(String catalog, String schema,
                String table, String columnNamePattern) throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getColumnPrivileges: "
                        + catalog + ", " + schema + ", " + table,
                        LoggingConnection.this);
                return super.getColumnPrivileges(catalog, schema,
                    table, columnNamePattern);
            }

            @Override
            public ResultSet getColumns(String catalog, String schemaPattern,
                String tableNamePattern, String columnNamePattern)
                throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getColumns: "
                        + catalog + ", " + schemaPattern + ", "
                        + tableNamePattern + ", " + columnNamePattern,
                        LoggingConnection.this);
                return super.getColumns(catalog, schemaPattern,
                    tableNamePattern, columnNamePattern);
            }

            @Override
            public ResultSet getCrossReference(String primaryCatalog,
                String primarySchema, String primaryTable,
                String foreignCatalog, String foreignSchema,
                String foreignTable) throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getCrossReference: "
                        + primaryCatalog + ", " + primarySchema + ", "
                        + primaryTable + ", " + foreignCatalog + ", "
                        + foreignSchema + ", " + foreignSchema,
                        LoggingConnection.this);
                return super.getCrossReference(primaryCatalog, primarySchema,
                    primaryTable, foreignCatalog, foreignSchema, foreignTable);
            }

            @Override
            public ResultSet getExportedKeys(String catalog, String schema,
                String table) throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getExportedKeys: "
                        + catalog + ", " + schema + ", " + table,
                        LoggingConnection.this);
                return super.getExportedKeys(catalog, schema, table);
            }

            @Override
            public ResultSet getImportedKeys(String catalog, String schema,
                String table) throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getImportedKeys: "
                        + catalog + ", " + schema + ", " + table,
                        LoggingConnection.this);
                return super.getImportedKeys(catalog, schema, table);
            }

            @Override
            public ResultSet getIndexInfo(String catalog, String schema,
                String table, boolean unique, boolean approximate)
                throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getIndexInfo: "
                        + catalog + ", " + schema + ", " + table,
                        LoggingConnection.this);
                return super.getIndexInfo(catalog, schema, table, unique,
                    approximate);
            }

            @Override
            public ResultSet getPrimaryKeys(String catalog, String schema,
                String table) throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getPrimaryKeys: "
                        + catalog + ", " + schema + ", " + table,
                        LoggingConnection.this);
                return super.getPrimaryKeys(catalog, schema, table);
            }

            @Override
            public ResultSet getProcedureColumns(String catalog,
                String schemaPattern, String procedureNamePattern,
                String columnNamePattern) throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getProcedureColumns: "
                        + catalog + ", " + schemaPattern + ", "
                        + procedureNamePattern + ", " + columnNamePattern,
                        LoggingConnection.this);
                return super.getProcedureColumns(catalog, schemaPattern,
                    procedureNamePattern, columnNamePattern);
            }

            @Override
            public ResultSet getProcedures(String catalog,
                String schemaPattern, String procedureNamePattern)
                throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getProcedures: "
                        + catalog + ", " + schemaPattern + ", "
                        + procedureNamePattern, LoggingConnection.this);
                return super.getProcedures(catalog, schemaPattern,
                    procedureNamePattern);
            }

            @Override
            public ResultSet getSchemas() throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getSchemas", LoggingConnection.this);
                return super.getSchemas();
            }

            @Override
            public ResultSet getTablePrivileges(String catalog,
                String schemaPattern, String tableNamePattern)
                throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getTablePrivileges", LoggingConnection.this);
                return super.getTablePrivileges(catalog, schemaPattern,
                    tableNamePattern);
            }

            @Override
            public ResultSet getTables(String catalog, String schemaPattern,
                String tableNamePattern, String[] types) throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getTables: "
                        + catalog + ", " + schemaPattern + ", "
                        + tableNamePattern, LoggingConnection.this);
                return super.getTables(catalog, schemaPattern,
                    tableNamePattern, types);
            }

            @Override
            public ResultSet getTableTypes() throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getTableTypes", LoggingConnection.this);
                return super.getTableTypes();
            }

            @Override
            public ResultSet getTypeInfo() throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getTypeInfo", LoggingConnection.this);
                return super.getTypeInfo();
            }

            @Override
            public ResultSet getUDTs(String catalog, String schemaPattern,
                String typeNamePattern, int[] types) throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getUDTs", LoggingConnection.this);
                return super.getUDTs(catalog, schemaPattern,
                    typeNamePattern, types);
            }

            @Override
            public ResultSet getVersionColumns(String catalog,
                String schema, String table) throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("getVersionColumns: "
                        + catalog + ", " + schema + ", " + table,
                        LoggingConnection.this);
                return super.getVersionColumns(catalog, schema, table);
            }
        }

        /**
         * Statement wrapper that logs SQL to the parent data source and
         * remembers the last piece of SQL to be executed on it.
         */
        protected class LoggingStatement extends DelegatingStatement {

            private String _sql = null;

            public LoggingStatement(Statement stmnt) throws SQLException {
                super(stmnt, LoggingConnection.this);
            }

            private LoggingResultSet newLoggingResultSet(ResultSet rs, Statement stmnt) {
                return new LoggingResultSet(rs, stmnt);
            }

            @Override
            public void appendInfo(StringBuffer buf) {
                if (_sql != null) {
                    buf.append(" ");
                    if (_formatter != null) {
                        buf.append(SEP);
                        buf.append(_formatter.prettyPrint(_sql));
                    } else {
                        buf.append(_sql);
                    }
                }
            }

            @Override
            protected ResultSet wrapResult(ResultSet rs, boolean wrap) {
                if (!wrap || rs == null)
                    return super.wrapResult(rs, wrap);
                return newLoggingResultSet(rs, this);
            }

            @Override
            public void cancel() throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("cancel " + this, LoggingConnection.this);
                super.cancel();
            }

            @Override
            protected ResultSet executeQuery(String sql, boolean wrap)
                throws SQLException {
                _sql = sql;
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeQuery(sql, wrap);
                } catch (SQLException se) {
                    err = wrap(se, LoggingStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    handleSQLErrors(LoggingStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String sql) throws SQLException {
                _sql = sql;
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(sql);
                } catch (SQLException se) {
                    err = wrap(se, LoggingStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    handleSQLErrors(LoggingStatement.this, err);
                }
            }

            @Override
            public boolean execute(String sql) throws SQLException {
                _sql = sql;
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(sql);
                } catch (SQLException se) {
                    err = wrap(se, LoggingStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    handleSQLErrors(LoggingStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String sql, int i) throws SQLException {
                _sql = sql;
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(sql, i);
                } catch (SQLException se) {
                    err = wrap(se, LoggingStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    handleSQLErrors(LoggingStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String sql, int[] ia) throws SQLException {
                _sql = sql;
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(sql, ia);
                } catch (SQLException se) {
                    err = wrap(se, LoggingStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    handleSQLErrors(LoggingStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String sql, String[] sa) throws SQLException {
                _sql = sql;
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(sql, sa);
                } catch (SQLException se) {
                    err = wrap(se, LoggingStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    handleSQLErrors(LoggingStatement.this, err);
                }
            }

            @Override
            public boolean execute(String sql, int i) throws SQLException {
                _sql = sql;
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(sql, i);
                } catch (SQLException se) {
                    err = wrap(se, LoggingStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    handleSQLErrors(LoggingStatement.this, err);
                }
            }

            @Override
            public boolean execute(String sql, int[] ia) throws SQLException {
                _sql = sql;
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(sql, ia);
                } catch (SQLException se) {
                    err = wrap(se, LoggingStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    handleSQLErrors(LoggingStatement.this, err);
                }
            }

            @Override
            public boolean execute(String sql, String[] sa) throws SQLException {
                _sql = sql;
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(sql, sa);
                } catch (SQLException se) {
                    err = wrap(se, LoggingStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    handleSQLErrors(LoggingStatement.this, err);
                }
            }
        }

        protected class LoggingPreparedStatement
            extends DelegatingPreparedStatement {

            private final String _sql;
            private List<String> _params = null;
            private List<List<String>> _paramBatch = null;
            // When batching is used, this variable contains the index into the
            // last successfully executed batched statement.
            int batchedRowsBaseIndex = 0;

            public LoggingPreparedStatement(PreparedStatement stmnt, String sql)
                throws SQLException {
                super(stmnt, LoggingConnection.this);
                _sql = sql;
            }

            private LoggingResultSet newLoggingResultSet(ResultSet rs,
                PreparedStatement stmnt) {
                return new LoggingResultSet(rs, stmnt);
            }

            @Override
            protected ResultSet wrapResult(ResultSet rs, boolean wrap) {
                if (!wrap || rs == null)
                    return super.wrapResult(rs, wrap);
                return newLoggingResultSet(rs, this);
            }

            @Override
            protected ResultSet executeQuery(String sql, boolean wrap)
                throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeQuery(sql, wrap);
                } catch (SQLException se) {
                    err = wrap(se, LoggingPreparedStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String sql) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(sql);
                } catch (SQLException se) {
                    err =  wrap(se, LoggingPreparedStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public boolean execute(String sql) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(sql);
                } catch (SQLException se) {
                    err = wrap(se, LoggingPreparedStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            protected ResultSet executeQuery(boolean wrap) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeQuery(wrap);
                } catch (SQLException se) {
                    err = wrap(se, LoggingPreparedStatement.this, _sql);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public int executeUpdate() throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate();
                } catch (SQLException se) {
                    err = wrap(se, LoggingPreparedStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public int[] executeBatch() throws SQLException {
                int indexOfFirstFailedObject = -1;

                logBatchSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    int[] toReturn = super.executeBatch();
                    //executeBatch is called any time the number of batched statements
                    //is equal to, or less than, batchLimit.  In the 'catch' block below,
                    //the logic seeks to find an index based on the current executeBatch
                    //results.  This is fine when executeBatch is only called once, but
                    //if executeBatch is called many times, the _paramsBatch will continue
                    //to grow, as such, to index into _paramsBatch, we need to take into
                    //account the number of times executeBatch is called in or der to
                    //correctly index into _paramsBatch.  To that end, each time executeBatch
                    //is called, lets get the size of _paramBatch.  This will effectively
                    //tell us the index of the last successfully executed batch statement.
                    //If an exception is caused, then we know that _paramBatch.size was
                    //the index of the LAST row to successfully execute.
                    if (_paramBatch != null){
                        batchedRowsBaseIndex = _paramBatch.size();
                    }
                    return toReturn;
                } catch (SQLException se) {
                    // if the exception is a BatchUpdateException, and
                    // we are tracking parameters, then set the current
                    // parameter set to be the index of the failed
                    // statement so that the ReportingSQLException will
                    // show the correct param(s)
                    if (se instanceof BatchUpdateException
                        && _paramBatch != null && shouldTrackParameters()) {
                        int[] count = ((BatchUpdateException) se).
                            getUpdateCounts();
                        if (count != null && count.length <= _paramBatch.size())
                        {
                            for (int i = 0; i < count.length; i++) {
                                // -3 is Statement.STATEMENT_FAILED, but is
                                // only available in JDK 1.4+
                                if (count[i] == Statement.EXECUTE_FAILED) {
                                    indexOfFirstFailedObject = i;
                                    break;
                                }
                            }

                            // no -3 element: it may be that the server stopped
                            // processing, so the size of the count will be
                            // the index
                            //See the Javadoc for 'getUpdateCounts'; a provider
                            //may stop processing when the first failure occurs,
                            //as such, it may only return 'UpdateCounts' for the
                            //first few which pass.  As such, the failed
                            //index is 'count.length', NOT count.length+1.  That
                            //is, if the provider ONLY returns the first few that
                            //passes (i.e. say an array of [1,1] is returned) then
                            //length is 2, and since _paramBatch starts at 0, we
                            //don't want to use length+1 as that will give us the
                            //wrong index.
                            if (indexOfFirstFailedObject == -1){
                                indexOfFirstFailedObject = count.length;
                            }

                            //Finally, whatever the index is at this point, add batchedRowsBaseIndex
                            //to it to get the final index.  Recall, we need to start our index from the
                            //last batch which successfully executed.
                            indexOfFirstFailedObject += batchedRowsBaseIndex;

                            // set the current params to the saved values
                            if (indexOfFirstFailedObject < _paramBatch.size())
                                _params = (List<String>) _paramBatch.get(indexOfFirstFailedObject);
                        }
                    }
                    err = wrap(se, LoggingPreparedStatement.this, indexOfFirstFailedObject);
                    throw err;
                } finally {
                    logTime(start);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public boolean execute() throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute();
                } catch (SQLException se) {
                    err = wrap(se, LoggingPreparedStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String s, int i) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(s, i);
                } catch (SQLException se) {
                    err = wrap(se, LoggingPreparedStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String s, int[] ia) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(s, ia);
                } catch (SQLException se) {
                    err = wrap(se, LoggingPreparedStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String s, String[] sa) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(s, sa);
                } catch (SQLException se) {
                    err = wrap(se, LoggingPreparedStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public boolean execute(String s, int i) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(s, i);
                } catch (SQLException se) {
                    err = wrap(se, LoggingPreparedStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public boolean execute(String s, int[] ia) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(s, ia);
                } catch (SQLException se) {
                    err = wrap(se, LoggingPreparedStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public boolean execute(String s, String[] sa) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(s, sa);
                } catch (SQLException se) {
                    err = wrap(se, LoggingPreparedStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingPreparedStatement.this, err);
                }
            }

            @Override
            public void cancel() throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("cancel " + this + ": " + _sql,
                        LoggingConnection.this);

                super.cancel();
            }

            @Override
            public void setNull(int i1, int i2) throws SQLException {
                setLogParameter(i1, "null", null);
                super.setNull(i1, i2);
            }

            @Override
            public void setBoolean(int i, boolean b) throws SQLException {
                setLogParameter(i, b);
                super.setBoolean(i, b);
            }

            @Override
            public void setByte(int i, byte b) throws SQLException {
                setLogParameter(i, b);
                super.setByte(i, b);
            }

            @Override
            public void setShort(int i, short s) throws SQLException {
                setLogParameter(i, s);
                super.setShort(i, s);
            }

            @Override
            public void setInt(int i1, int i2) throws SQLException {
                setLogParameter(i1, i2);
                super.setInt(i1, i2);
            }

            @Override
            public void setLong(int i, long l) throws SQLException {
                setLogParameter(i, l);
                super.setLong(i, l);
            }

            @Override
            public void setFloat(int i, float f) throws SQLException {
                setLogParameter(i, f);
                super.setFloat(i, f);
            }

            @Override
            public void setDouble(int i, double d) throws SQLException {
                setLogParameter(i, d);
                super.setDouble(i, d);
            }

            @Override
            public void setBigDecimal(int i, BigDecimal bd)
                throws SQLException {
                setLogParameter(i, "BigDecimal", bd);
                super.setBigDecimal(i, bd);
            }

            @Override
            public void setString(int i, String s) throws SQLException {
                setLogParameter(i, "String", s);
                super.setString(i, s);
            }

            @Override
            public void setBytes(int i, byte[] b) throws SQLException {
                setLogParameter(i, "byte[]", b);
                super.setBytes(i, b);
            }

            @Override
            public void setDate(int i, Date d) throws SQLException {
                setLogParameter(i, "Date", d);
                super.setDate(i, d);
            }

            @Override
            public void setTime(int i, Time t) throws SQLException {
                setLogParameter(i, "Time", t);
                super.setTime(i, t);
            }

            @Override
            public void setTimestamp(int i, Timestamp t) throws SQLException {
                setLogParameter(i, "Timestamp", t);
                super.setTimestamp(i, t);
            }

            @Override
            public void setAsciiStream(int i1, InputStream is, int i2)
                throws SQLException {
                setLogParameter(i1, "InputStream", is);
                super.setAsciiStream(i1, is, i2);
            }

            @Override
            @Deprecated
            public void setUnicodeStream(int i1, InputStream is, int i2)
                throws SQLException {
                setLogParameter(i1, "InputStream", is);
                super.setUnicodeStream(i2, is, i2);
            }

            @Override
            public void setBinaryStream(int i1, InputStream is, int i2)
                throws SQLException {
                setLogParameter(i1, "InputStream", is);
                super.setBinaryStream(i1, is, i2);
            }

            @Override
            public void setBinaryStream(int i1, InputStream is)
            throws SQLException {
            	setLogParameter(i1, "InputStream", is);
            	super.setBinaryStream(i1, is);
            }

            @Override
            public void clearParameters() throws SQLException {
                clearLogParameters(false);
                super.clearParameters();
            }

            @Override
            public void setObject(int i1, Object o, int i2, int i3)
                throws SQLException {
                setLogParameter(i1, "Object", o);
                super.setObject(i1, o, i2, i3);
            }

            @Override
            public void setObject(int i1, Object o, int i2)
                throws SQLException {
                setLogParameter(i1, "Object", o);
                super.setObject(i1, o, i2);
            }

            @Override
            public void setObject(int i, Object o) throws SQLException {
                setLogParameter(i, "Object", o);
                super.setObject(i, o);
            }

            @Override
            public void addBatch() throws SQLException {
                if (_logs.isSQLEnabled())
                    _logs.logSQL("batching " + this, LoggingConnection.this);
                long start = System.currentTimeMillis();
                try {
                    super.addBatch();
                    if (shouldTrackParameters()) {
                        // make sure our list is initialized
                        if (_paramBatch == null)
                            _paramBatch = new ArrayList<>();
                        // copy parameters since they will be re-used
                        if (_params != null) {
                            List<String> copyParms =
                                    new ArrayList<>(_params);
                            _paramBatch.add(copyParms);
                        }
                        else
                            _paramBatch.add(null);
                    }
                }
                finally {
                    logTime(start);
                }
            }

            @Override
            public void setCharacterStream(int i1, Reader r, int i2)
                throws SQLException {
                setLogParameter(i1, "Reader", r);
                super.setCharacterStream(i1, r, i2);
            }

            @Override
            public void setRef(int i, Ref r) throws SQLException {
                setLogParameter(i, "Ref", r);
                super.setRef(i, r);
            }

            @Override
            public void setBlob(int i, Blob b) throws SQLException {
                setLogParameter(i, "Blob", b);
                super.setBlob(i, b);
            }

            @Override
            public void setClob(int i, Clob c) throws SQLException {
                setLogParameter(i, "Clob", c);
                super.setClob(i, c);
            }

            @Override
            public void setArray(int i, Array a) throws SQLException {
                setLogParameter(i, "Array", a);
                super.setArray(i, a);
            }

            @Override
            public ResultSetMetaData getMetaData() throws SQLException {
                return super.getMetaData();
            }

            @Override
            public void setDate(int i, Date d, Calendar c) throws SQLException {
                setLogParameter(i, "Date", d);
                super.setDate(i, d, c);
            }

            @Override
            public void setTime(int i, Time t, Calendar c) throws SQLException {
                setLogParameter(i, "Time", t);
                super.setTime(i, t, c);
            }

            @Override
            public void setTimestamp(int i, Timestamp t, Calendar c)
                throws SQLException {
                setLogParameter(i, "Timestamp", t);
                super.setTimestamp(i, t, c);
            }

            @Override
            public void setNull(int i1, int i2, String s) throws SQLException {
                setLogParameter(i1, "null", null);
                super.setNull(i1, i2, s);
            }

            @Override
            public void setURL(int i, URL u) throws SQLException {
                setLogParameter(i, "URL", u);
                super.setURL(i, u);
            }

            @Override
            protected void appendInfo(StringBuffer buf) {
                buf.append(" ");
                if (_formatter != null) {
                    buf.append(SEP);
                    buf.append(_formatter.prettyPrint(_sql));
                    buf.append(SEP);
                } else {
                    buf.append(_sql);
                }

                StringBuilder paramBuf = null;
                if (_params != null && !_params.isEmpty()) {
                    paramBuf = new StringBuilder();
                    for (Iterator<String> itr = _params.iterator(); itr
                        .hasNext();) {
                        if (_printParameters) {
                            paramBuf.append(itr.next());
                        } else {
                            paramBuf.append("?");
                            itr.next();
                        }
                        if (itr.hasNext()) {
                            paramBuf.append(", ");
                        }
                    }
                }

                if (paramBuf != null) {
                    if (!_prettyPrint) {
                        buf.append(" ");
                    }
                    buf.append("[params=").append(paramBuf.toString()).append("]");
                }
                super.appendInfo(buf);
            }

            private void clearLogParameters(boolean batch) {
                if (_params != null) {
                    _params.clear();
                }

                if (batch && _paramBatch != null) {
                    _paramBatch.clear();
                }
            }

            private boolean shouldTrackParameters() {
                return _printParameters || _logs.isSQLEnabled();
            }

            private void setLogParameter(int index, boolean val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(boolean) " + val);
            }

            private void setLogParameter(int index, byte val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(byte) " + val);
            }

            private void setLogParameter(int index, double val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(double) " + val);
            }

            private void setLogParameter(int index, float val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(float) " + val);
            }

            private void setLogParameter(int index, int val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(int) " + val);
            }

            private void setLogParameter(int index, long val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(long) " + val);
            }

            private void setLogParameter(int index, short val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(short) " + val);
            }

            private void setLogParameter(int index, String type, Object val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(" + type + ") " + val);
            }

            private void setLogParameter(int index, String val) {
                if (_params == null)
                    _params = new ArrayList<>();
                while (_params.size() < index)
                    _params.add(null);
                if (val.length() > 80)
                    val = val.substring(0, 77) + "...";
                _params.set(index - 1, val);
            }
        }

        /**
         * Warning-handling result set.
         */
        protected class LoggingResultSet extends DelegatingResultSet {

            public LoggingResultSet(ResultSet rs, Statement stmnt) {
                super(rs, stmnt);
            }

            @Override
            public boolean next() throws SQLException {
                SQLException err = null;
                try {
                    return super.next();
                } catch (SQLException se) {
                    err = se;
                    throw se;
                } finally {
                    handleSQLErrors(LoggingResultSet.this, err);
                }
            }

            @Override
            public void close() throws SQLException {
                SQLException err = null;
                try {
                    super.close();
                } catch (SQLException se) {
                    err = se;
                    throw se;
                } finally {
                    handleSQLErrors(LoggingResultSet.this, err);
                }
            }

            @Override
            public void beforeFirst() throws SQLException {
                SQLException err = null;
                try {
                    super.beforeFirst();
                } catch (SQLException se) {
                    err = se;
                    throw se;
                } finally {
                    handleSQLErrors(LoggingResultSet.this, err);
                }
            }

            @Override
            public void afterLast() throws SQLException {
                SQLException err = null;
                try {
                    super.afterLast();
                } catch (SQLException se) {
                    err = se;
                    throw se;
                } finally {
                    handleSQLErrors(LoggingResultSet.this, err);
                }
            }

            @Override
            public boolean first() throws SQLException {
                SQLException err = null;
                try {
                    return super.first();
                } catch (SQLException se) {
                    err = se;
                    throw se;
                } finally {
                    handleSQLErrors(LoggingResultSet.this, err);
                }
            }

            @Override
            public boolean last() throws SQLException {
                SQLException err = null;
                try {
                    return super.last();
                } catch (SQLException se) {
                    err = se;
                    throw se;
                } finally {
                    handleSQLErrors(LoggingResultSet.this, err);
                }
            }

            @Override
            public boolean absolute(int a) throws SQLException {
                SQLException err = null;
                try {
                    return super.absolute(a);
                } catch (SQLException se) {
                    err = se;
                    throw se;
                } finally {
                    handleSQLErrors(LoggingResultSet.this, err);
                }
            }

            @Override
            public boolean relative(int a) throws SQLException {
                SQLException err = null;
                try {
                    return super.relative(a);
                } catch (SQLException se) {
                    err = se;
                    throw se;
                } finally {
                    handleSQLErrors(LoggingResultSet.this, err);
                }
            }

            @Override
            public boolean previous() throws SQLException {
                SQLException err = null;
                try {
                    return super.previous();
                } catch (SQLException se) {
                    err = se;
                    throw se;
                } finally {
                    handleSQLErrors(LoggingResultSet.this, err);
                }
            }
        }

        /**
         * CallableStatement decorated with logging.
         * Similar to {@link LoggingPreparedStatement} but can not be extended
         * due to the existing delegation hierarchy.
         */
        protected class LoggingCallableStatement extends
            DelegatingCallableStatement {
            private final String _sql;
            private List<String> _params = null;
            private List<List<String>> _paramBatch = null;
            //When batching is used, this variable contains the index into the last
            //successfully executed batched statement.
            int batchedRowsBaseIndex = 0;

            public LoggingCallableStatement(CallableStatement stmt, String sql)
                throws SQLException {
        		super(stmt, LoggingConnection.this);
        		_sql = sql;
        	}

            private LoggingResultSet newLoggingResultSet(ResultSet rs,
                CallableStatement stmnt) {
                return new LoggingResultSet(rs, stmnt);
            }

            protected ResultSet wrapResult(ResultSet rs, boolean wrap) {
                if (!wrap || rs == null)
                    return super.wrapResult(wrap, rs);
                return newLoggingResultSet(rs, this);
            }

            @Override
            protected ResultSet executeQuery(String sql, boolean wrap)
                throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeQuery(sql, wrap);
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String sql) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(sql);
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public boolean execute(String sql) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(sql);
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this, sql);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            protected ResultSet executeQuery(boolean wrap) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeQuery(wrap);
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public int executeUpdate() throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate();
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public int[] executeBatch() throws SQLException {
                int indexOfFirstFailedObject = -1;

                logBatchSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    int[] toReturn = super.executeBatch();
                    //executeBatch is called any time the number of batched statements
                    //is equal to, or less than, batchLimit.  In the 'catch' block below,
                    //the logic seeks to find an index based on the current executeBatch
                    //results.  This is fine when executeBatch is only called once, but
                    //if executeBatch is called many times, the _paramsBatch will continue
                    //to grow, as such, to index into _paramsBatch, we need to take into
                    //account the number of times executeBatch is called in order to
                    //correctly index into _paramsBatch.  To that end, each time executeBatch
                    //is called, lets get the size of _paramBatch.  This will effectively
                    //tell us the index of the last successfully executed batch statement.
                    //If an exception is caused, then we know that _paramBatch.size was
                    //the index of the LAST row to successfully execute.
                    if (_paramBatch != null){
                        batchedRowsBaseIndex = _paramBatch.size();
                    }
                    return toReturn;
                } catch (SQLException se) {
                    // if the exception is a BatchUpdateException, and
                    // we are tracking parameters, then set the current
                    // parameter set to be the index of the failed
                    // statement so that the ReportingSQLException will
                    // show the correct param
                    if (se instanceof BatchUpdateException
                        && _paramBatch != null && shouldTrackParameters()) {
                        int[] count = ((BatchUpdateException) se).
                            getUpdateCounts();
                        if (count != null && count.length <= _paramBatch.size())
                        {
                            for (int i = 0; i < count.length; i++) {
                                // -3 is Statement.STATEMENT_FAILED, but is
                                // only available in JDK 1.4+
                                if (count[i] == Statement.EXECUTE_FAILED) {
                                    indexOfFirstFailedObject = i;
                                    break;
                                }
                            }

                            // no -3 element: it may be that the server stopped
                            // processing, so the size of the count will be
                            // the index
                            //See the Javadoc for 'getUpdateCounts'; a provider
                            //may stop processing when the first failure occurs,
                            //as such, it may only return 'UpdateCounts' for the
                            //first few which pass.  As such, the failed
                            //index is 'count.length', NOT count.length+1.  That
                            //is, if the provider ONLY returns the first few that
                            //passes (i.e. say an array of [1,1] is returned) then
                            //length is 2, and since _paramBatch starts at 0, we
                            //don't want to use length+1 as that will give us the
                            //wrong index.
                            if (indexOfFirstFailedObject == -1){
                                indexOfFirstFailedObject = count.length;
                            }

                            //Finally, whatever the index is at this point, add batchedRowsBaseIndex
                            //to it to get the final index.  Recall, we need to start our index from the
                            //last batch which successfully executed.
                            indexOfFirstFailedObject += batchedRowsBaseIndex;

                            // set the current params to the saved values
                            if (indexOfFirstFailedObject < _paramBatch.size()){
                                _params = (List<String>) _paramBatch.get(indexOfFirstFailedObject);
                            }
                        }
                    }
                    err = wrap(se, LoggingCallableStatement.this, indexOfFirstFailedObject);
                    throw err;
                } finally {
                    logTime(start);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public boolean execute() throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute();
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String s, int i) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(s, i);
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String s, int[] ia) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(s, ia);
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public int executeUpdate(String s, String[] sa) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.executeUpdate(s, sa);
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public boolean execute(String s, int i) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(s, i);
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public boolean execute(String s, int[] ia) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(s, ia);
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public boolean execute(String s, String[] sa) throws SQLException {
                logSQL(this);
                long start = System.currentTimeMillis();
                SQLException err = null;
                try {
                    return super.execute(s, sa);
                } catch (SQLException se) {
                    err = wrap(se, LoggingCallableStatement.this);
                    throw err;
                } finally {
                    logTime(start);
                    clearLogParameters(true);
                    handleSQLErrors(LoggingCallableStatement.this, err);
                }
            }

            @Override
            public void cancel() throws SQLException {
                if (_logs.isJDBCEnabled())
                    _logs.logJDBC("cancel " + this + ": " + _sql,
                        LoggingConnection.this);

                super.cancel();
            }

            @Override
            public void setNull(int i1, int i2) throws SQLException {
                setLogParameter(i1, "null", null);
                super.setNull(i1, i2);
            }

            @Override
            public void setBoolean(int i, boolean b) throws SQLException {
                setLogParameter(i, b);
                super.setBoolean(i, b);
            }

            @Override
            public void setByte(int i, byte b) throws SQLException {
                setLogParameter(i, b);
                super.setByte(i, b);
            }

            @Override
            public void setShort(int i, short s) throws SQLException {
                setLogParameter(i, s);
                super.setShort(i, s);
            }

            @Override
            public void setInt(int i1, int i2) throws SQLException {
                setLogParameter(i1, i2);
                super.setInt(i1, i2);
            }

            @Override
            public void setLong(int i, long l) throws SQLException {
                setLogParameter(i, l);
                super.setLong(i, l);
            }

            @Override
            public void setFloat(int i, float f) throws SQLException {
                setLogParameter(i, f);
                super.setFloat(i, f);
            }

            @Override
            public void setDouble(int i, double d) throws SQLException {
                setLogParameter(i, d);
                super.setDouble(i, d);
            }

            @Override
            public void setBigDecimal(int i, BigDecimal bd)
                throws SQLException {
                setLogParameter(i, "BigDecimal", bd);
                super.setBigDecimal(i, bd);
            }

            @Override
            public void setString(int i, String s) throws SQLException {
                setLogParameter(i, "String", s);
                super.setString(i, s);
            }

            @Override
            public void setBytes(int i, byte[] b) throws SQLException {
                setLogParameter(i, "byte[]", b);
                super.setBytes(i, b);
            }

            @Override
            public void setDate(int i, Date d) throws SQLException {
                setLogParameter(i, "Date", d);
                super.setDate(i, d);
            }

            @Override
            public void setTime(int i, Time t) throws SQLException {
                setLogParameter(i, "Time", t);
                super.setTime(i, t);
            }

            @Override
            public void setTimestamp(int i, Timestamp t) throws SQLException {
                setLogParameter(i, "Timestamp", t);
                super.setTimestamp(i, t);
            }

            @Override
            public void setAsciiStream(int i1, InputStream is, int i2)
                throws SQLException {
                setLogParameter(i1, "InputStream", is);
                super.setAsciiStream(i1, is, i2);
            }

            @Override
            @Deprecated
            public void setUnicodeStream(int i1, InputStream is, int i2)
                throws SQLException {
                setLogParameter(i1, "InputStream", is);
                super.setUnicodeStream(i2, is, i2);
            }

            @Override
            public void setBinaryStream(int i1, InputStream is, int i2)
                throws SQLException {
                setLogParameter(i1, "InputStream", is);
                super.setBinaryStream(i1, is, i2);
            }

            @Override
            public void clearParameters() throws SQLException {
                clearLogParameters(false);
                super.clearParameters();
            }

            @Override
            public void setObject(int i1, Object o, int i2, int i3)
                throws SQLException {
                setLogParameter(i1, "Object", o);
                super.setObject(i1, o, i2, i3);
            }

            @Override
            public void setObject(int i1, Object o, int i2)
                throws SQLException {
                setLogParameter(i1, "Object", o);
                super.setObject(i1, o, i2);
            }

            @Override
            public void setObject(int i, Object o) throws SQLException {
                setLogParameter(i, "Object", o);
                super.setObject(i, o);
            }

            @Override
            public void addBatch() throws SQLException {
                if (_logs.isSQLEnabled())
                    _logs.logSQL("batching " + this, LoggingConnection.this);
                long start = System.currentTimeMillis();
                try {
                    super.addBatch();
                    if (shouldTrackParameters()) {
                        // make sure our list is initialized
                        if (_paramBatch == null)
                            _paramBatch = new ArrayList<>();
                        // copy parameters since they will be re-used
                        if (_params != null) {
                            List<String> copyParams =
                                new ArrayList<>(_params);
                            _paramBatch.add(copyParams);
                        }
                        else
                            _paramBatch.add(null);
                    }
                }
                finally {
                    logTime(start);
                }
            }

            @Override
            public void setCharacterStream(int i1, Reader r, int i2)
                throws SQLException {
                setLogParameter(i1, "Reader", r);
                super.setCharacterStream(i1, r, i2);
            }

            @Override
            public void setRef(int i, Ref r) throws SQLException {
                setLogParameter(i, "Ref", r);
                super.setRef(i, r);
            }

            @Override
            public void setBlob(int i, Blob b) throws SQLException {
                setLogParameter(i, "Blob", b);
                super.setBlob(i, b);
            }

            @Override
            public void setClob(int i, Clob c) throws SQLException {
                setLogParameter(i, "Clob", c);
                super.setClob(i, c);
            }

            @Override
            public void setArray(int i, Array a) throws SQLException {
                setLogParameter(i, "Array", a);
                super.setArray(i, a);
            }

            @Override
            public ResultSetMetaData getMetaData() throws SQLException {
                return super.getMetaData();
            }

            @Override
            public void setDate(int i, Date d, Calendar c) throws SQLException {
                setLogParameter(i, "Date", d);
                super.setDate(i, d, c);
            }

            @Override
            public void setTime(int i, Time t, Calendar c) throws SQLException {
                setLogParameter(i, "Time", t);
                super.setTime(i, t, c);
            }

            @Override
            public void setTimestamp(int i, Timestamp t, Calendar c)
                throws SQLException {
                setLogParameter(i, "Timestamp", t);
                super.setTimestamp(i, t, c);
            }

            @Override
            public void setNull(int i1, int i2, String s) throws SQLException {
                setLogParameter(i1, "null", null);
                super.setNull(i1, i2, s);
            }

            @Override
            public void setURL(int i, URL u) throws SQLException {
                setLogParameter(i, "URL", u);
                super.setURL(i, u);
            }

            @Override
            protected void appendInfo(StringBuffer buf) {
                buf.append(" ");
                if (_formatter != null) {
                    buf.append(SEP);
                    buf.append(_formatter.prettyPrint(_sql));
                    buf.append(SEP);
                } else {
                    buf.append(_sql);
                }

                StringBuilder paramBuf = null;
                if (_params != null && !_params.isEmpty()) {
                    paramBuf = new StringBuilder();
                    for (Iterator<String> itr = _params.iterator(); itr
                        .hasNext();) {
                        paramBuf.append(itr.next());
                        if (itr.hasNext())
                            paramBuf.append(", ");
                    }
                }

                if (paramBuf != null) {
                    if (!_prettyPrint)
                        buf.append(" ");
                    buf.append("[params=").
                        append(paramBuf.toString()).append("]");
                }
                super.appendInfo(buf);
            }

            protected void clearLogParameters(boolean batch) {
                if (_params != null)
                    _params.clear();
                if (batch && _paramBatch != null)
                    _paramBatch.clear();
            }

            private boolean shouldTrackParameters() {
                return _printParameters || _logs.isSQLEnabled();
            }

            private void setLogParameter(int index, boolean val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(boolean) " + val);
            }

            private void setLogParameter(int index, byte val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(byte) " + val);
            }

            private void setLogParameter(int index, double val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(double) " + val);
            }

            private void setLogParameter(int index, float val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(float) " + val);
            }

            private void setLogParameter(int index, int val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(int) " + val);
            }

            private void setLogParameter(int index, long val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(long) " + val);
            }

            private void setLogParameter(int index, short val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(short) " + val);
            }

            private void setLogParameter(int index, String type, Object val) {
                if (shouldTrackParameters())
                    setLogParameter(index, "(" + type + ") " + val);
            }

            private void setLogParameter(int index, String val) {
                if (_params == null)
                    _params = new ArrayList<>();
                while (_params.size() < index)
                    _params.add(null);
                if (val.length() > 80)
                    val = val.substring(0, 77) + "...";
                _params.set(index - 1, val);
            }
        }
    }
}
