/*
 * 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);
            }

