    001    /**
    002     * Copyright 2007-2008 Arthur Blake
    003     *
    004     * Licensed under the Apache License, Version 2.0 (the "License");
    005     * you may not use this file except in compliance with the License.
    006     * You may obtain a copy of the License at
    007     *
    008     *    http://www.apache.org/licenses/LICENSE-2.0
    009     *
    010     * Unless required by applicable law or agreed to in writing, software
    011     * distributed under the License is distributed on an "AS IS" BASIS,
    012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    013     * See the License for the specific language governing permissions and
    014     * limitations under the License.
    015     */
    016    package net.sf.log4jdbc;
    017
    018    import java.sql.Connection;
    019    import java.sql.DatabaseMetaData;
    020    import java.sql.Driver;
    021    import java.sql.DriverManager;
    022    import java.sql.DriverPropertyInfo;
    023    import java.sql.SQLException;
    024    import java.util.Enumeration;
    025    import java.util.HashMap;
    026    import java.util.Iterator;
    027    import java.util.Map;
    028    import java.util.Properties;
    029    import java.util.Set;
    030    import java.util.TreeSet;
    031
    032    /**
    033     * A JDBC driver which is a facade that delegates to one or more real underlying
    034     * JDBC drivers.  The driver will spy on any other JDBC driver that is loaded,
    035     * simply by prepending <code>jdbc:log4</code> to the normal jdbc driver URL
    036     * used by any other JDBC driver. The driver also loads several well known
    037     * drivers at class load time, so that this driver can be "dropped in" to any
    038     * java program that uses these drivers without making any code changes.
    039     * The well known driver classes that are loaded are:
    040     * <p/>
    041     * <p/>
    042     * <code>
    043     * <ul>
    044     * <li>oracle.jdbc.driver.OracleDriver</li>
    045     * <li>com.sybase.jdbc2.jdbc.SybDriver</li>
    046     * <li>net.sourceforge.jtds.jdbc.Driver</li>
    047     * <li>com.microsoft.jdbc.sqlserver.SQLServerDriver</li>
    048     * <li>weblogic.jdbc.sqlserver.SQLServerDriver</li>
    049     * <li>com.informix.jdbc.IfxDriver</li>
    050     * <li>org.apache.derby.jdbc.ClientDriver</li>
    051     * <li>org.apache.derby.jdbc.EmbeddedDriver</li>
    052     * <li>org.hsqldb.jdbcDriver</li>
    053     * <li>org.h2.Driver</li>
    054     * </ul>
    055     * </code>
    056     * <p/>
    057     * <p/>
    058     * Additional drivers can be set via a system property: <b>log4jdbc.drivers</b>
    059     * This can be either a single driver class name or a list of comma separated
    060     * driver class names.
    061     * <p/>
    062     * If any of the above driver classes cannot be loaded, the driver continues on
    063     * without failing.
    064     * <p/>
    065     * Note that the <code>getMajorVersion</code>, <code>getMinorVersion</code> and
    066     * <code>jdbcCompliant</code> method calls attempt to delegate to the last
    067     * underlying driver requested through any other call that accepts a JDBC URL.
    068     * <p/>
    069     * This can cause unexpected behavior in certain circumstances.  For example,
    070     * if one of these 3 methods is called before any underlying driver has been
    071     * established, then they will return default values that might not be correct
    072     * in all situations.  Similarly, if this spy driver is used to spy on more than
    073     * one underlying driver concurrently, the values returned by these 3 method
    074     * calls may change depending on what the last underlying driver used was at the
    075     * time.  This will not usually be a problem, since the driver is retrieved by
    076     * it's URL from the DriverManager in the first place (thus establishing an
    077     * underlying real driver), and in most applications their is only one database.
    078     *
    079     * @author Arthur Blake
    080     */
    081    public class DriverSpy implements Driver
    082    {
    083      /**
    084       * The last actual, underlying driver that was requested via a URL.
    085       */
    086      private Driver lastUnderlyingDriverRequested;
    087
    088      /**
    089       * Maps driver class names to RdbmsSpecifics objects for each kind of
    090       * database.
    091       */
    092      private static Map rdbmsSpecifics;
    093
    094      static final SpyLogDelegator log = SpyLogFactory.getSpyLogDelegator();
    095
    096      /**
    097       * Optional package prefix to use for finding application generating point of
    098       * SQL.
    099       */
    100      static String DebugStackPrefix;
    101
    102      /**
    103       * Flag to indicate debug trace info should be from the calling application
    104       * point of view (true if DebugStackPrefix is set.)
    105       */
    106      static boolean TraceFromApplication;
    107
    108      /**
    109       * Flag to indicate if a warning should be shown if SQL takes more than
    110       * SqlTimingWarnThresholdMsec milliseconds to run.  See below.
    111       */
    112      static boolean SqlTimingWarnThresholdEnabled;
    113
    114      /**
    115       * An amount of time in milliseconds for which SQL that executed taking this
    116       * long or more to run shall cause a warning message to be generated on the
    117       * SQL timing logger.
    118       *
    119       * This threshold will <i>ONLY</i> be used if SqlTimingWarnThresholdEnabled
    120       * is true.
    121       */
    122      static long SqlTimingWarnThresholdMsec;
    123
    124      /**
    125       * Flag to indicate if an error should be shown if SQL takes more than
    126       * SqlTimingErrorThresholdMsec milliseconds to run.  See below.
    127       */
    128      static boolean SqlTimingErrorThresholdEnabled;
    129
    130      /**
    131       * An amount of time in milliseconds for which SQL that executed taking this
    132       * long or more to run shall cause an error message to be generated on the
    133       * SQL timing logger.
    134       *
    135       * This threshold will <i>ONLY</i> be used if SqlTimingErrorThresholdEnabled
    136       * is true.
    137       */
    138      static long SqlTimingErrorThresholdMsec;
    139
    140      /**
    141       * Get a Long option from a system property and
    142       * log a debug message about this.
    143       *
    144       * @param propName System property key.
    145       *
    146       * @return the value of that System property key, converted
    147       * to a Long.  Or null if not defined or is invalid.
    148       */
    149      private static Long getLongOption(String propName)
    150      {
    151        String propValue = System.getProperty(propName);
    152        Long longPropValue = null;
    153        if (propValue == null)
    154        {
    155          log.debug("x " + propName + " is not defined");
    156        }
    157        else
    158        {
    159          try
    160          {
    161            longPropValue = new Long(Long.parseLong(propValue));
    162            log.debug("  " + propName + " = " + longPropValue);
    163          }
    164          catch (NumberFormatException n)
    165          {
    166            log.debug("x " + propName + " \"" + propValue  +
    167              "\" is not a valid long value");
    168          }
    169        }
    170        return longPropValue;
    171      }
    172
    173      /**
    174       * Get a String option from a system property and
    175       * log a debug message about this.
    176       *
    177       * @param propName System property key.
    178       * @return the value of that System property key.
    179       */
    180      private static String getStringOption(String propName)
    181      {
    182        String propValue = System.getProperty(propName);
    183        if (propValue == null || propValue.length()==0)
    184        {
    185          log.debug("x " + propName + " is not defined");
    186          propValue = null; // force to null, even if empty String
    187        }
    188        else
    189        {
    190          log.debug("  " + propName + " = " + propValue);
    191        }
    192        return propValue;
    193      }
    194
    195      static
    196      {
    197        log.debug("... log4jdbc initializing ...");
    198
    199        // look for additional driver specified in system properties
    200        DebugStackPrefix = getStringOption("log4jdbc.debug.stack.prefix");
    201        TraceFromApplication = DebugStackPrefix != null;
    202
    203        Long thresh = getLongOption("log4jdbc.sqltiming.warn.threshold");
    204        SqlTimingWarnThresholdEnabled = (thresh != null);
    205        if (SqlTimingWarnThresholdEnabled)
    206        {
    207          SqlTimingWarnThresholdMsec = thresh.longValue();
    208        }
    209
    210        thresh = getLongOption("log4jdbc.sqltiming.error.threshold");
    211        SqlTimingErrorThresholdEnabled = (thresh != null);
    212        if (SqlTimingErrorThresholdEnabled)
    213        {
    214          SqlTimingErrorThresholdMsec = thresh.longValue();
    215        }
    216
    217        // The Set of drivers that the log4jdbc driver will preload at instantiation
    218        // time.  The driver can spy on any driver type, it's just a little bit
    219        // easier to configure log4jdbc if it's one of these types!
    220
    221        Set subDrivers = new TreeSet();
    222        subDrivers.add("oracle.jdbc.driver.OracleDriver");
    223        subDrivers.add("com.sybase.jdbc2.jdbc.SybDriver");
    224        subDrivers.add("net.sourceforge.jtds.jdbc.Driver");
    225        subDrivers.add("com.microsoft.jdbc.sqlserver.SQLServerDriver");
    226        subDrivers.add("weblogic.jdbc.sqlserver.SQLServerDriver");
    227        subDrivers.add("com.informix.jdbc.IfxDriver");
    228        subDrivers.add("org.apache.derby.jdbc.ClientDriver");
    229        subDrivers.add("org.apache.derby.jdbc.EmbeddedDriver");
    230        subDrivers.add("com.mysql.jdbc.Driver");
    231        subDrivers.add("org.postgresql.Driver");
    232        subDrivers.add("org.hsqldb.jdbcDriver");
    233        subDrivers.add("org.h2.Driver");
    234
    235        // look for additional driver specified in system properties
    236        String moreDrivers = getStringOption("log4jdbc.drivers");
    237
    238        if (moreDrivers != null)
    239        {
    240          String[] moreDriversArr = moreDrivers.split(",");
    241
    242          for (int i = 0; i < moreDriversArr.length; i++)
    243          {
    244            subDrivers.add(moreDriversArr[i]);
    245            log.debug ("    will look for additional driver " + moreDriversArr[i]);
    246          }
    247        }
    248
    249        try
    250        {
    251          DriverManager.registerDriver(new DriverSpy());
    252        }
    253        catch (SQLException s)
    254        {
    255          // this exception should never be thrown, JDBC just defines it
    256          // for completeness
    257          throw (RuntimeException) new RuntimeException
    258            ("could not register log4jdbc driver!").initCause(s);
    259        }
    260
    261        // instantiate all the supported drivers and remove
    262        // those not found
    263        String driverClass;
    264        for (Iterator i = subDrivers.iterator(); i.hasNext();)
    265        {
    266          driverClass = (String) i.next();
    267          try
    268          {
    269            Class.forName(driverClass);
    270            log.debug("  FOUND DRIVER " + driverClass);
    271          }
    272          catch (Throwable c)
    273          {
    274            i.remove();
    275          }
    276        }
    277
    278        if (subDrivers.size() == 0)
    279        {
    280          log.debug("WARNING!  " +
    281            "log4jdbc couldn't find any underlying jdbc drivers.");
    282        }
    283
    284        SqlServerRdbmsSpecifics sqlServer = new SqlServerRdbmsSpecifics();
    285
    286        /** create lookup Map for specific rdbms formatters */
    287        rdbmsSpecifics = new HashMap();
    288        rdbmsSpecifics.put("oracle.jdbc.driver.OracleDriver",
    289          new OracleRdbmsSpecifics());
    290        rdbmsSpecifics.put("net.sourceforge.jtds.jdbc.Driver", sqlServer);
    291        rdbmsSpecifics.put("com.microsoft.jdbc.sqlserver.SQLServerDriver",
    292          sqlServer);
    293        rdbmsSpecifics.put("weblogic.jdbc.sqlserver.SQLServerDriver", sqlServer);
    294
    295        log.debug("... log4jdbc initialized! ...");
    296      }
    297
    298      static RdbmsSpecifics defaultRdbmsSpecifics = new RdbmsSpecifics();
    299
    300      /**
    301       * Get the RdbmsSpecifics object for a given Connection.
    302       *
    303       * @param conn JDBC connection to get RdbmsSpecifics for.
    304       * @return RdbmsSpecifics for the given connection.
    305       */
    306      static RdbmsSpecifics getRdbmsSpecifics(Connection conn)
    307      {
    308        String driverName = "";
    309        try
    310        {
    311          DatabaseMetaData dbm = conn.getMetaData();
    312          driverName = dbm.getDriverName();
    313        }
    314        catch (SQLException s)
    315        {
    316          // silently fail
    317        }
    318
    319        log.debug("driver name is " + driverName);
    320
    321        RdbmsSpecifics r = (RdbmsSpecifics) rdbmsSpecifics.get(driverName);
    322
    323        if (r == null)
    324        {
    325          return defaultRdbmsSpecifics;
    326        }
    327        else
    328        {
    329          return r;
    330        }
    331      }
    332
    333      /**
    334       * Default constructor.
    335       */
    336      public DriverSpy()
    337      {
    338      }
    339
    340      /**
    341       * Get the major version of the driver.  This call will be delegated to the
    342       * underlying driver that is being spied upon (if there is no underlying
    343       * driver found, then 1 will be returned.)
    344       *
    345       * @return the major version of the JDBC driver.
    346       */
    347      public int getMajorVersion()
    348      {
    349        if (lastUnderlyingDriverRequested == null)
    350        {
    351          return 1;
    352        }
    353        else
    354        {
    355          return lastUnderlyingDriverRequested.getMajorVersion();
    356        }
    357      }
    358
    359      /**
    360       * Get the minor version of the driver.  This call will be delegated to the
    361       * underlying driver that is being spied upon (if there is no underlying
    362       * driver found, then 0 will be returned.)
    363       *
    364       * @return the minor version of the JDBC driver.
    365       */
    366      public int getMinorVersion()
    367      {
    368        if (lastUnderlyingDriverRequested == null)
    369        {
    370          return 0;
    371        }
    372        else
    373        {
    374          return lastUnderlyingDriverRequested.getMinorVersion();
    375        }
    376      }
    377
    378      /**
    379       * Report whether the underlying driver is JDBC compliant.  If there is no
    380       * underlying driver, false will be returned, because the driver cannot
    381       * actually do any work without an underlying driver.
    382       *
    383       * @return <code>true</code> if the underlying driver is JDBC Compliant;
    384       *         <code>false</code> otherwise.
    385       */
    386      public boolean jdbcCompliant()
    387      {
    388        return lastUnderlyingDriverRequested != null &&
    389          lastUnderlyingDriverRequested.jdbcCompliant();
    390      }
    391
    392      /**
    393       * Returns true if this is a <code>jdbc:log4</code> URL and if the URL is for
    394       * an underlying driver that this DriverSpy can spy on.
    395       *
    396       * @param url JDBC URL.
    397       *
    398       * @return true if this Driver can handle the URL.
    399       *
    400       * @throws SQLException if a database access error occurs
    401       */
    402      public boolean acceptsURL(String url) throws SQLException
    403      {
    404        Driver d = getUnderlyingDriver(url);
    405        if (d != null)
    406        {
    407          lastUnderlyingDriverRequested = d;
    408          return true;
    409        }
    410        else
    411        {
    412          return false;
    413        }
    414      }
    415
    416      /**
    417       * Given a <code>jdbc:log4</code> type URL, find the underlying real driver
    418       * that accepts the URL.
    419       *
    420       * @param url JDBC connection URL.
    421       *
    422       * @return Underlying driver for the given URL. Null is returned if the URL is
    423       *         not a <code>jdbc:log4</code> type URL or there is no underlying
    424       *         driver that accepts the URL.
    425       *
    426       * @throws SQLException if a database access error occurs.
    427       */
    428      private Driver getUnderlyingDriver(String url) throws SQLException
    429      {
    430        if (url.startsWith("jdbc:log4"))
    431        {
    432          url = url.substring(9);
    433
    434          Enumeration e = DriverManager.getDrivers();
    435
    436          Driver d;
    437          while (e.hasMoreElements())
    438          {
    439            d = (Driver) e.nextElement();
    440
    441            if (d.acceptsURL(url))
    442            {
    443              return d;
    444            }
    445          }
    446        }
    447        return null;
    448      }
    449
    450      /**
    451       * Get a Connection to the database from the underlying driver that this
    452       * DriverSpy is spying on.  If logging is not enabled, an actual Connection to
    453       * the database returned.  If logging is enabled, a ConnectionSpy object which
    454       * wraps the real Connection is returned.
    455       *
    456       * @param url  JDBC connection URL
    457       * .
    458       * @param info a list of arbitrary string tag/value pairs as
    459       *             connection arguments. Normally at least a "user" and
    460       *             "password" property should be included.
    461       *
    462       * @return     a <code>Connection</code> object that represents a
    463       *             connection to the URL.
    464       *
    465       * @throws SQLException if a database access error occurs
    466       */
    467      public Connection connect(String url, Properties info) throws SQLException
    468      {
    469        Driver d = getUnderlyingDriver(url);
    470        if (d == null)
    471        {
    472          return null;
    473        }
    474
    475        // get actual URL that the real driver expects
    476        // (strip off "jdbc:log4" from url)
    477        url = url.substring(9);
    478
    479        lastUnderlyingDriverRequested = d;
    480        Connection c = d.connect(url, info);
    481
    482        if (c == null)
    483        {
    484          throw new SQLException("invalid or unknown driver url: " + url);
    485        }
    486        if (log.isJdbcLoggingEnabled())
    487        {
    488          ConnectionSpy cspy = new ConnectionSpy(c);
    489          RdbmsSpecifics r = null;
    490          String dclass = d.getClass().getName();
    491          if (dclass != null && dclass.length() > 0)
    492          {
    493            r = (RdbmsSpecifics) rdbmsSpecifics.get(dclass);
    494          }
    495
    496          if (r == null)
    497          {
    498            r = defaultRdbmsSpecifics;
    499          }
    500          cspy.setRdbmsSpecifics(r);
    501          return cspy;
    502        }
    503        else
    504        {
    505          return c;
    506        }
    507      }
    508
    509      /**
    510       * Gets information about the possible properties for the underlying driver.
    511       *
    512       * @param url  the URL of the database to which to connect
    513       *
    514       * @param info a proposed list of tag/value pairs that will be sent on
    515       *             connect open
    516       * @return     an array of <code>DriverPropertyInfo</code> objects describing
    517       *             possible properties.  This array may be an empty array if no
    518       *             properties are required.
    519       *
    520       * @throws SQLException if a database access error occurs
    521       */
    522      public DriverPropertyInfo[] getPropertyInfo(String url, Properties info)
    523        throws SQLException
    524      {
    525        Driver d = getUnderlyingDriver(url);
    526        if (d == null)
    527        {
    528          return new DriverPropertyInfo[0];
    529        }
    530
    531        lastUnderlyingDriverRequested = d;
    532        return d.getPropertyInfo(url, info);
    533      }
    534    }