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    }