/**
   * Create a new ConnectionSpy that wraps a given Connection.
   *
   * @param realConnection "real" Connection that this ConnectionSpy wraps.
   * @param rdbmsSpecifics the RdbmsSpecifics object for formatting logging appropriate for the
   *     Rdbms used.
   */
  public ConnectionSpy(Connection realConnection, RdbmsSpecifics rdbmsSpecifics) {
    if (rdbmsSpecifics == null) {
      rdbmsSpecifics = DriverSpy.defaultRdbmsSpecifics;
    }
    setRdbmsSpecifics(rdbmsSpecifics);
    if (realConnection == null) {
      throw new IllegalArgumentException("Must pass in a non null real Connection");
    }
    this.realConnection = realConnection;
    log = SpyLogFactory.getSpyLogDelegator();

    synchronized (connectionTracker) {
      connectionNumber = new Integer(++lastConnectionNumber);
      connectionTracker.put(connectionNumber, this);
    }
    log.connectionOpened(this);
    reportReturn("new Connection");
  }
/**
 * A JDBC driver which is a facade that delegates to one or more real underlying JDBC drivers. The
 * driver will spy on any other JDBC driver that is loaded, simply by prepending <code>jdbc:log4
 * </code> to the normal jdbc driver URL used by any other JDBC driver. The driver, by default, also
 * loads several well known drivers at class load time, so that this driver can be "dropped in" to
 * any Java program that uses these drivers without making any code changes.
 *
 * <p>The well known driver classes that are loaded are:
 *
 * <ul>
 *   <li><code>oracle.jdbc.driver.OracleDriver</code>
 *   <li><code>com.sybase.jdbc2.jdbc.SybDriver</code>
 *   <li><code>net.sourceforge.jtds.jdbc.Driver</code>
 *   <li><code>com.microsoft.jdbc.sqlserver.SQLServerDriver</code>
 *   <li><code>com.microsoft.sqlserver.jdbc.SQLServerDriver</code>
 *   <li><code>weblogic.jdbc.sqlserver.SQLServerDriver</code>
 *   <li><code>com.informix.jdbc.IfxDriver</code>
 *   <li><code>org.apache.derby.jdbc.ClientDriver</code>
 *   <li><code>org.apache.derby.jdbc.EmbeddedDriver</code>
 *   <li><code>com.mysql.jdbc.Driver</code>
 *   <li><code>org.postgresql.Driver</code>
 *   <li><code>org.hsqldb.jdbcDriver</code>
 *   <li><code>org.h2.Driver</code>
 * </ul>
 *
 * <p>Additional drivers can be set via a property: <b>log4jdbc.drivers</b> This can be either a
 * single driver class name or a list of comma separated driver class names.
 *
 * <p>The autoloading behavior can be disabled by setting a property:
 * <b>log4jdbc.auto.load.popular.drivers</b> to false. If that is done, then the only drivers that
 * log4jdbc will attempt to load are the ones specified in <b>log4jdbc.drivers</b>.
 *
 * <p>If any of the above driver classes cannot be loaded, the driver continues on without failing.
 *
 * <p>Note that the <code>getMajorVersion</code>, <code>getMinorVersion</code> and <code>
 * jdbcCompliant</code> method calls attempt to delegate to the last underlying driver requested
 * through any other call that accepts a JDBC URL.
 *
 * <p>This can cause unexpected behavior in certain circumstances. For example, if one of these 3
 * methods is called before any underlying driver has been established, then they will return
 * default values that might not be correct in all situations. Similarly, if this spy driver is used
 * to spy on more than one underlying driver concurrently, the values returned by these 3 method
 * calls may change depending on what the last underlying driver used was at the time. This will not
 * usually be a problem, since the driver is retrieved by it's URL from the DriverManager in the
 * first place (thus establishing an underlying real driver), and in most applications their is only
 * one database.
 *
 * @author Arthur Blake
 */
public class DriverSpy implements Driver {
  /** The last actual, underlying driver that was requested via a URL. */
  private Driver lastUnderlyingDriverRequested;

  /** Maps driver class names to RdbmsSpecifics objects for each kind of database. */
  private static Map rdbmsSpecifics;

  static final SpyLogDelegator log = SpyLogFactory.getSpyLogDelegator();

  /** Optional package prefix to use for finding application generating point of SQL. */
  static String DebugStackPrefix;

  /**
   * Flag to indicate debug trace info should be from the calling application point of view (true if
   * DebugStackPrefix is set.)
   */
  static boolean TraceFromApplication;

  /**
   * Flag to indicate if a warning should be shown if SQL takes more than SqlTimingWarnThresholdMsec
   * milliseconds to run. See below.
   */
  static boolean SqlTimingWarnThresholdEnabled;

  /**
   * An amount of time in milliseconds for which SQL that executed taking this long or more to run
   * shall cause a warning message to be generated on the SQL timing logger.
   *
   * <p>This threshold will <i>ONLY</i> be used if SqlTimingWarnThresholdEnabled is true.
   */
  static long SqlTimingWarnThresholdMsec;

  /**
   * Flag to indicate if an error should be shown if SQL takes more than SqlTimingErrorThresholdMsec
   * milliseconds to run. See below.
   */
  static boolean SqlTimingErrorThresholdEnabled;

  /**
   * An amount of time in milliseconds for which SQL that executed taking this long or more to run
   * shall cause an error message to be generated on the SQL timing logger.
   *
   * <p>This threshold will <i>ONLY</i> be used if SqlTimingErrorThresholdEnabled is true.
   */
  static long SqlTimingErrorThresholdMsec;

  /**
   * When dumping boolean values, dump them as 'true' or 'false'. If this option is not set, they
   * will be dumped as 1 or 0 as many databases do not have a boolean type, and this allows for more
   * portable sql dumping.
   */
  static boolean DumpBooleanAsTrueFalse;

  /**
   * When dumping SQL, if this is greater than 0, than the SQL will be broken up into lines that are
   * no longer than this value.
   */
  static int DumpSqlMaxLineLength;

  /**
   * If this is true, display a special warning in the log along with the SQL when the application
   * uses a Statement (as opposed to a PreparedStatement.) Using Statements for frequently used SQL
   * can sometimes result in performance and/or security problems.
   */
  static boolean StatementUsageWarn;

  /**
   * Options to more finely control which types of SQL statements will be dumped, when dumping SQL.
   * By default all 5 of the following will be true. If any one is set to false, then that
   * particular type of SQL will not be dumped.
   */
  static boolean DumpSqlSelect;

  static boolean DumpSqlInsert;
  static boolean DumpSqlUpdate;
  static boolean DumpSqlDelete;
  static boolean DumpSqlCreate;

  // only true if one ore more of the above 4 flags are false.
  static boolean DumpSqlFilteringOn;

  /** If true, add a semilcolon to the end of each SQL dump. */
  static boolean DumpSqlAddSemicolon;

  /**
   * If dumping in debug mode, dump the full stack trace. This will result in a VERY voluminous
   * output, but can be very useful under some circumstances.
   */
  static boolean DumpFullDebugStackTrace;

  /** Attempt to Automatically load a set of popular JDBC drivers? */
  static boolean AutoLoadPopularDrivers;

  /** Trim SQL before logging it? */
  static boolean TrimSql;

  /**
   * Remove extra Lines in the SQL that consist of only white space? Only when 2 or more lines in a
   * row like this occur, will the extra lines (beyond 1) be removed.
   */
  static boolean TrimExtraBlankLinesInSql;

  /**
   * Coldfusion typically calls PreparedStatement.getGeneratedKeys() after every SQL update call,
   * even if it's not warranted. This typically produces an exception that is ignored by Coldfusion.
   * If this flag is true, then any exception generated by this method is also ignored by log4jdbc.
   */
  static boolean SuppressGetGeneratedKeysException;

  /**
   * Get a Long option from a property and log a debug message about this.
   *
   * @param props Properties to get option from.
   * @param propName property key.
   * @return the value of that property key, converted to a Long. Or null if not defined or is
   *     invalid.
   */
  private static Long getLongOption(Properties props, String propName) {
    String propValue = props.getProperty(propName);
    Long longPropValue = null;
    if (propValue == null) {
      log.debug("x " + propName + " is not defined");
    } else {
      try {
        longPropValue = new Long(Long.parseLong(propValue));
        log.debug("  " + propName + " = " + longPropValue);
      } catch (NumberFormatException n) {
        log.debug("x " + propName + " \"" + propValue + "\" is not a valid number");
      }
    }
    return longPropValue;
  }

  /**
   * Get a Long option from a property and log a debug message about this.
   *
   * @param props Properties to get option from.
   * @param propName property key.
   * @return the value of that property key, converted to a Long. Or null if not defined or is
   *     invalid.
   */
  private static Long getLongOption(Properties props, String propName, long defaultValue) {
    String propValue = props.getProperty(propName);
    Long longPropValue;
    if (propValue == null) {
      log.debug("x " + propName + " is not defined (using default of " + defaultValue + ")");
      longPropValue = new Long(defaultValue);
    } else {
      try {
        longPropValue = new Long(Long.parseLong(propValue));
        log.debug("  " + propName + " = " + longPropValue);
      } catch (NumberFormatException n) {
        log.debug(
            "x "
                + propName
                + " \""
                + propValue
                + "\" is not a valid number (using default of "
                + defaultValue
                + ")");
        longPropValue = new Long(defaultValue);
      }
    }
    return longPropValue;
  }

  /**
   * Get a String option from a property and log a debug message about this.
   *
   * @param props Properties to get option from.
   * @param propName property key.
   * @return the value of that property key.
   */
  private static String getStringOption(Properties props, String propName) {
    String propValue = props.getProperty(propName);
    if (propValue == null || propValue.length() == 0) {
      log.debug("x " + propName + " is not defined");
      propValue = null; // force to null, even if empty String
    } else {
      log.debug("  " + propName + " = " + propValue);
    }
    return propValue;
  }

  /**
   * Get a boolean option from a property and log a debug message about this.
   *
   * @param props Properties to get option from.
   * @param propName property name to get.
   * @param defaultValue default value to use if undefined.
   * @return boolean value found in property, or defaultValue if no property found.
   */
  private static boolean getBooleanOption(Properties props, String propName, boolean defaultValue) {
    String propValue = props.getProperty(propName);
    boolean val;
    if (propValue == null) {
      log.debug("x " + propName + " is not defined (using default value " + defaultValue + ")");
      return defaultValue;
    } else {
      propValue = propValue.trim().toLowerCase();
      if (propValue.length() == 0) {
        val = defaultValue;
      } else {
        val = "true".equals(propValue) || "yes".equals(propValue) || "on".equals(propValue);
      }
    }
    log.debug("  " + propName + " = " + val);
    return val;
  }

  static {
    log.debug("... log4jdbc initializing ...");

    InputStream propStream = DriverSpy.class.getResourceAsStream("/log4jdbc.properties");

    Properties props = new Properties(System.getProperties());
    if (propStream != null) {
      try {
        props.load(propStream);
      } catch (IOException e) {
        log.debug(
            "ERROR!  io exception loading "
                + "log4jdbc.properties from classpath: "
                + e.getMessage());
      } finally {
        try {
          propStream.close();
        } catch (IOException e) {
          log.debug("ERROR!  io exception closing property file stream: " + e.getMessage());
        }
      }
      log.debug("  log4jdbc.properties loaded from classpath");
    } else {
      log.debug("  log4jdbc.properties not found on classpath");
    }

    // look for additional driver specified in properties
    DebugStackPrefix = getStringOption(props, "log4jdbc.debug.stack.prefix");
    TraceFromApplication = DebugStackPrefix != null;

    Long thresh = getLongOption(props, "log4jdbc.sqltiming.warn.threshold");
    SqlTimingWarnThresholdEnabled = (thresh != null);
    if (SqlTimingWarnThresholdEnabled) {
      SqlTimingWarnThresholdMsec = thresh.longValue();
    }

    thresh = getLongOption(props, "log4jdbc.sqltiming.error.threshold");
    SqlTimingErrorThresholdEnabled = (thresh != null);
    if (SqlTimingErrorThresholdEnabled) {
      SqlTimingErrorThresholdMsec = thresh.longValue();
    }

    DumpBooleanAsTrueFalse = getBooleanOption(props, "log4jdbc.dump.booleanastruefalse", false);

    DumpSqlMaxLineLength = getLongOption(props, "log4jdbc.dump.sql.maxlinelength", 90L).intValue();

    DumpFullDebugStackTrace = getBooleanOption(props, "log4jdbc.dump.fulldebugstacktrace", false);

    StatementUsageWarn = getBooleanOption(props, "log4jdbc.statement.warn", false);

    DumpSqlSelect = getBooleanOption(props, "log4jdbc.dump.sql.select", true);
    DumpSqlInsert = getBooleanOption(props, "log4jdbc.dump.sql.insert", true);
    DumpSqlUpdate = getBooleanOption(props, "log4jdbc.dump.sql.update", true);
    DumpSqlDelete = getBooleanOption(props, "log4jdbc.dump.sql.delete", true);
    DumpSqlCreate = getBooleanOption(props, "log4jdbc.dump.sql.create", true);

    DumpSqlFilteringOn =
        !(DumpSqlSelect && DumpSqlInsert && DumpSqlUpdate && DumpSqlDelete && DumpSqlCreate);

    DumpSqlAddSemicolon = getBooleanOption(props, "log4jdbc.dump.sql.addsemicolon", false);

    AutoLoadPopularDrivers = getBooleanOption(props, "log4jdbc.auto.load.popular.drivers", true);

    TrimSql = getBooleanOption(props, "log4jdbc.trim.sql", true);

    TrimExtraBlankLinesInSql = getBooleanOption(props, "log4jdbc.trim.sql.extrablanklines", true);

    SuppressGetGeneratedKeysException =
        getBooleanOption(props, "log4jdbc.suppress.generated.keys.exception", false);

    // The Set of drivers that the log4jdbc driver will preload at instantiation
    // time.  The driver can spy on any driver type, it's just a little bit
    // easier to configure log4jdbc if it's one of these types!

    Set subDrivers = new TreeSet();

    if (AutoLoadPopularDrivers) {
      subDrivers.add("oracle.jdbc.driver.OracleDriver");
      subDrivers.add("oracle.jdbc.OracleDriver");
      subDrivers.add("com.sybase.jdbc2.jdbc.SybDriver");
      subDrivers.add("net.sourceforge.jtds.jdbc.Driver");

      // MS driver for Sql Server 2000
      subDrivers.add("com.microsoft.jdbc.sqlserver.SQLServerDriver");

      // MS driver for Sql Server 2005
      subDrivers.add("com.microsoft.sqlserver.jdbc.SQLServerDriver");

      subDrivers.add("weblogic.jdbc.sqlserver.SQLServerDriver");
      subDrivers.add("com.informix.jdbc.IfxDriver");
      subDrivers.add("org.apache.derby.jdbc.ClientDriver");
      subDrivers.add("org.apache.derby.jdbc.EmbeddedDriver");
      subDrivers.add("com.mysql.jdbc.Driver");
      subDrivers.add("org.postgresql.Driver");
      subDrivers.add("org.hsqldb.jdbcDriver");
      subDrivers.add("org.h2.Driver");
    }

    // look for additional driver specified in properties
    String moreDrivers = getStringOption(props, "log4jdbc.drivers");

    if (moreDrivers != null) {
      String[] moreDriversArr = moreDrivers.split(",");

      for (int i = 0; i < moreDriversArr.length; i++) {
        subDrivers.add(moreDriversArr[i]);
        log.debug("    will look for specific driver " + moreDriversArr[i]);
      }
    }

    try {
      DriverManager.registerDriver(new DriverSpy());
    } catch (SQLException s) {
      // this exception should never be thrown, JDBC just defines it
      // for completeness
      throw (RuntimeException)
          new RuntimeException("could not register log4jdbc driver!").initCause(s);
    }

    // instantiate all the supported drivers and remove
    // those not found
    String driverClass;
    for (Iterator i = subDrivers.iterator(); i.hasNext(); ) {
      driverClass = (String) i.next();
      try {
        Class.forName(driverClass);
        log.debug("  FOUND DRIVER " + driverClass);
      } catch (Throwable c) {
        i.remove();
      }
    }

    if (subDrivers.size() == 0) {
      log.debug("WARNING!  " + "log4jdbc couldn't find any underlying jdbc drivers.");
    }

    SqlServerRdbmsSpecifics sqlServer = new SqlServerRdbmsSpecifics();
    OracleRdbmsSpecifics oracle = new OracleRdbmsSpecifics();
    MySqlRdbmsSpecifics mySql = new MySqlRdbmsSpecifics();

    /** create lookup Map for specific rdbms formatters */
    rdbmsSpecifics = new HashMap();
    rdbmsSpecifics.put("oracle.jdbc.driver.OracleDriver", oracle);
    rdbmsSpecifics.put("oracle.jdbc.OracleDriver", oracle);
    rdbmsSpecifics.put("net.sourceforge.jtds.jdbc.Driver", sqlServer);
    rdbmsSpecifics.put("com.microsoft.jdbc.sqlserver.SQLServerDriver", sqlServer);
    rdbmsSpecifics.put("weblogic.jdbc.sqlserver.SQLServerDriver", sqlServer);
    rdbmsSpecifics.put("com.mysql.jdbc.Driver", mySql);

    log.debug("... log4jdbc initialized! ...");
  }

  static RdbmsSpecifics defaultRdbmsSpecifics = new RdbmsSpecifics();

  /**
   * Get the RdbmsSpecifics object for a given Connection.
   *
   * @param conn JDBC connection to get RdbmsSpecifics for.
   * @return RdbmsSpecifics for the given connection.
   */
  static RdbmsSpecifics getRdbmsSpecifics(Connection conn) {
    String driverName = "";
    try {
      DatabaseMetaData dbm = conn.getMetaData();
      driverName = dbm.getDriverName();
    } catch (SQLException s) {
      // silently fail
    }

    log.debug("driver name is " + driverName);

    RdbmsSpecifics r = (RdbmsSpecifics) rdbmsSpecifics.get(driverName);

    if (r == null) {
      return defaultRdbmsSpecifics;
    } else {
      return r;
    }
  }

  /** Default constructor. */
  public DriverSpy() {}

  /**
   * Get the major version of the driver. This call will be delegated to the underlying driver that
   * is being spied upon (if there is no underlying driver found, then 1 will be returned.)
   *
   * @return the major version of the JDBC driver.
   */
  public int getMajorVersion() {
    if (lastUnderlyingDriverRequested == null) {
      return 1;
    } else {
      return lastUnderlyingDriverRequested.getMajorVersion();
    }
  }

  /**
   * Get the minor version of the driver. This call will be delegated to the underlying driver that
   * is being spied upon (if there is no underlying driver found, then 0 will be returned.)
   *
   * @return the minor version of the JDBC driver.
   */
  public int getMinorVersion() {
    if (lastUnderlyingDriverRequested == null) {
      return 0;
    } else {
      return lastUnderlyingDriverRequested.getMinorVersion();
    }
  }

  /**
   * Report whether the underlying driver is JDBC compliant. If there is no underlying driver, false
   * will be returned, because the driver cannot actually do any work without an underlying driver.
   *
   * @return <code>true</code> if the underlying driver is JDBC Compliant; <code>false</code>
   *     otherwise.
   */
  public boolean jdbcCompliant() {
    return lastUnderlyingDriverRequested != null && lastUnderlyingDriverRequested.jdbcCompliant();
  }

  /**
   * Returns true if this is a <code>jdbc:log4</code> URL and if the URL is for an underlying driver
   * that this DriverSpy can spy on.
   *
   * @param url JDBC URL.
   * @return true if this Driver can handle the URL.
   * @throws SQLException if a database access error occurs
   */
  public boolean acceptsURL(String url) throws SQLException {
    Driver d = getUnderlyingDriver(url);
    if (d != null) {
      lastUnderlyingDriverRequested = d;
      return true;
    } else {
      return false;
    }
  }

  /**
   * Given a <code>jdbc:log4</code> type URL, find the underlying real driver that accepts the URL.
   *
   * @param url JDBC connection URL.
   * @return Underlying driver for the given URL. Null is returned if the URL is not a <code>
   *     jdbc:log4</code> type URL or there is no underlying driver that accepts the URL.
   * @throws SQLException if a database access error occurs.
   */
  private Driver getUnderlyingDriver(String url) throws SQLException {
    if (url.startsWith("jdbc:log4")) {
      url = url.substring(9);

      Enumeration e = DriverManager.getDrivers();

      Driver d;
      while (e.hasMoreElements()) {
        d = (Driver) e.nextElement();

        if (d.acceptsURL(url)) {
          return d;
        }
      }
    }
    return null;
  }

  /**
   * Get a Connection to the database from the underlying driver that this DriverSpy is spying on.
   * If logging is not enabled, an actual Connection to the database returned. If logging is
   * enabled, a ConnectionSpy object which wraps the real Connection is returned.
   *
   * @param url JDBC connection URL .
   * @param info a list of arbitrary string tag/value pairs as connection arguments. Normally at
   *     least a "user" and "password" property should be included.
   * @return a <code>Connection</code> object that represents a connection to the URL.
   * @throws SQLException if a database access error occurs
   */
  public Connection connect(String url, Properties info) throws SQLException {
    Driver d = getUnderlyingDriver(url);
    if (d == null) {
      return null;
    }

    // get actual URL that the real driver expects
    // (strip off "jdbc:log4" from url)
    url = url.substring(9);

    lastUnderlyingDriverRequested = d;
    Connection c = d.connect(url, info);

    if (c == null) {
      throw new SQLException("invalid or unknown driver url: " + url);
    }
    if (log.isJdbcLoggingEnabled()) {
      ConnectionSpy cspy = new ConnectionSpy(c);
      RdbmsSpecifics r = null;
      String dclass = d.getClass().getName();
      if (dclass != null && dclass.length() > 0) {
        r = (RdbmsSpecifics) rdbmsSpecifics.get(dclass);
      }

      if (r == null) {
        r = defaultRdbmsSpecifics;
      }
      cspy.setRdbmsSpecifics(r);
      return cspy;
    } else {
      return c;
    }
  }

  /**
   * Gets information about the possible properties for the underlying driver.
   *
   * @param url the URL of the database to which to connect
   * @param info a proposed list of tag/value pairs that will be sent on connect open
   * @return an array of <code>DriverPropertyInfo</code> objects describing possible properties.
   *     This array may be an empty array if no properties are required.
   * @throws SQLException if a database access error occurs
   */
  public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
    Driver d = getUnderlyingDriver(url);
    if (d == null) {
      return new DriverPropertyInfo[0];
    }

    lastUnderlyingDriverRequested = d;
    return d.getPropertyInfo(url, info);
  }

  public Logger getParentLogger() throws SQLFeatureNotSupportedException {
    if (lastUnderlyingDriverRequested != null) {
      return lastUnderlyingDriverRequested.getParentLogger();
    } else {
      throw new SQLFeatureNotSupportedException();
    }
  }
}