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>com.microsoft.sqlserver.jdbc.SQLServerDriver</li>
049     * <li>weblogic.jdbc.sqlserver.SQLServerDriver</li>
050     * <li>com.informix.jdbc.IfxDriver</li>
051     * <li>org.apache.derby.jdbc.ClientDriver</li>
052     * <li>org.apache.derby.jdbc.EmbeddedDriver</li>
053     * <li>com.mysql.jdbc.Driver</li>
054     * <li>org.postgresql.Driver</li>
055     * <li>org.hsqldb.jdbcDriver</li>
056     * <li>org.h2.Driver</li>
057     * </ul>
058     * </code>
059     * <p/>
060     * <p/>
061     * Additional drivers can be set via a system property: <b>log4jdbc.drivers</b>
062     * This can be either a single driver class name or a list of comma separated
063     * driver class names.
064     * <p/>
065     * If any of the above driver classes cannot be loaded, the driver continues on
066     * without failing.
067     * <p/>
068     * Note that the <code>getMajorVersion</code>, <code>getMinorVersion</code> and
069     * <code>jdbcCompliant</code> method calls attempt to delegate to the last
070     * underlying driver requested through any other call that accepts a JDBC URL.
071     * <p/>
072     * This can cause unexpected behavior in certain circumstances.  For example,
073     * if one of these 3 methods is called before any underlying driver has been
074     * established, then they will return default values that might not be correct
075     * in all situations.  Similarly, if this spy driver is used to spy on more than
076     * one underlying driver concurrently, the values returned by these 3 method
077     * calls may change depending on what the last underlying driver used was at the
078     * time.  This will not usually be a problem, since the driver is retrieved by
079     * it's URL from the DriverManager in the first place (thus establishing an
080     * underlying real driver), and in most applications their is only one database.
081     *
082     * @author Arthur Blake
083     */
084    public class DriverSpy implements Driver
085    {
086      /**
087       * The last actual, underlying driver that was requested via a URL.
088       */
089      private Driver lastUnderlyingDriverRequested;
090    
091      /**
092       * Maps driver class names to RdbmsSpecifics objects for each kind of
093       * database.
094       */
095      private static Map rdbmsSpecifics;
096    
097      static final SpyLogDelegator log = SpyLogFactory.getSpyLogDelegator();
098    
099      /**
100       * Optional package prefix to use for finding application generating point of
101       * SQL.
102       */
103      static String DebugStackPrefix;
104    
105      /**
106       * Flag to indicate debug trace info should be from the calling application
107       * point of view (true if DebugStackPrefix is set.)
108       */
109      static boolean TraceFromApplication;
110    
111      /**
112       * Flag to indicate if a warning should be shown if SQL takes more than
113       * SqlTimingWarnThresholdMsec milliseconds to run.  See below.
114       */
115      static boolean SqlTimingWarnThresholdEnabled;
116    
117      /**
118       * An amount of time in milliseconds for which SQL that executed taking this
119       * long or more to run shall cause a warning message to be generated on the
120       * SQL timing logger.
121       *
122       * This threshold will <i>ONLY</i> be used if SqlTimingWarnThresholdEnabled
123       * is true.
124       */
125      static long SqlTimingWarnThresholdMsec;
126    
127      /**
128       * Flag to indicate if an error should be shown if SQL takes more than
129       * SqlTimingErrorThresholdMsec milliseconds to run.  See below.
130       */
131      static boolean SqlTimingErrorThresholdEnabled;
132    
133      /**
134       * An amount of time in milliseconds for which SQL that executed taking this
135       * long or more to run shall cause an error message to be generated on the
136       * SQL timing logger.
137       *
138       * This threshold will <i>ONLY</i> be used if SqlTimingErrorThresholdEnabled
139       * is true.
140       */
141      static long SqlTimingErrorThresholdMsec;
142    
143      /**
144       * When dumping boolean values, dump them as 'true' or 'false'.
145       * If this option is not set, they will be dumped as 1 or 0 as many
146       * databases do not have a boolean type, and this allows for more
147       * portable sql dumping.
148       */
149      static boolean DumpBooleanAsTrueFalse;
150    
151      /**
152       * When dumping SQL, if this is greater than 0, than the SQL will
153       * be broken up into lines that are no longer than this value.
154       */
155      static int DumpSqlMaxLineLength;
156    
157      /**
158       * Options to more finely control which types of SQL statements will
159       * be dumped, when dumping SQL.
160       * By default all 5 of the following will be true.  If any one is set to
161       * false, then that particular type of SQL will not be dumped.
162       */
163      static boolean DumpSqlSelect;
164      static boolean DumpSqlInsert;
165      static boolean DumpSqlUpdate;
166      static boolean DumpSqlDelete;
167      static boolean DumpSqlCreate;
168    
169      // only true if one ore more of the above 4 flags are false.
170      static boolean DumpSqlFilteringOn;
171    
172      /**
173       * If true, add a semilcolon to the end of each SQL dump.
174       */
175      static boolean DumpSqlAddSemicolon;
176    
177      /**
178       * If dumping in debug mode, dump the full stack trace.
179       * This will result in a VERY voluminous output, but can be very useful
180       * under some circumstances.
181       */
182      static boolean DumpFullDebugStackTrace;
183    
184      /**
185       * Get a Long option from a system property and
186       * log a debug message about this.
187       *
188       * @param propName System property key.
189       *
190       * @return the value of that System property key, converted
191       * to a Long.  Or null if not defined or is invalid.
192       */
193      private static Long getLongOption(String propName)
194      {
195        String propValue = System.getProperty(propName);
196        Long longPropValue = null;
197        if (propValue == null)
198        {
199          log.debug("x " + propName + " is not defined");
200        }
201        else
202        {
203          try
204          {
205            longPropValue = new Long(Long.parseLong(propValue));
206            log.debug("  " + propName + " = " + longPropValue);
207          }
208          catch (NumberFormatException n)
209          {
210            log.debug("x " + propName + " \"" + propValue  +
211              "\" is not a valid number");
212          }
213        }
214        return longPropValue;
215      }
216    
217      /**
218       * Get a Long option from a system property and
219       * log a debug message about this.
220       *
221       * @param propName System property key.
222       *
223       * @return the value of that System property key, converted
224       * to a Long.  Or null if not defined or is invalid.
225       */
226      private static Long getLongOption(String propName, long defaultValue)
227      {
228        String propValue = System.getProperty(propName);
229        Long longPropValue;
230        if (propValue == null)
231        {
232          log.debug("x " + propName + " is not defined (using default of " + defaultValue +")");
233          longPropValue = new Long(defaultValue);
234        }
235        else
236        {
237          try
238          {
239            longPropValue = new Long(Long.parseLong(propValue));
240            log.debug("  " + propName + " = " + longPropValue);
241          }
242          catch (NumberFormatException n)
243          {
244            log.debug("x " + propName + " \"" + propValue  +
245              "\" is not a valid number (using default of " + defaultValue +")");
246            longPropValue = new Long(defaultValue);
247          }
248        }
249        return longPropValue;
250      }
251    
252      /**
253       * Get a String option from a system property and
254       * log a debug message about this.
255       *
256       * @param propName System property key.
257       * @return the value of that System property key.
258       */
259      private static String getStringOption(String propName)
260      {
261        String propValue = System.getProperty(propName);
262        if (propValue == null || propValue.length()==0)
263        {
264          log.debug("x " + propName + " is not defined");
265          propValue = null; // force to null, even if empty String
266        }
267        else
268        {
269          log.debug("  " + propName + " = " + propValue);
270        }
271        return propValue;
272      }
273    
274      /**
275       * Get a boolean option from a system property and
276       * log a debug message about this.
277       *
278       * @param propName property name to get.
279       * @param defaultValue default value to use if undefined.
280       * 
281       * @return boolean value found in property, or defaultValue if no property
282       *         found.
283       */
284      private static boolean getBooleanOption(String propName, boolean defaultValue)
285      {
286        String propValue = System.getProperty(propName);
287        boolean val;
288        if (propValue == null)
289        {
290          log.debug("x " + propName + " is not defined (using default value "
291            + defaultValue + ")");
292          return defaultValue;
293        }
294        else
295        {
296          propValue = propValue.trim().toLowerCase();
297          if (propValue.length() == 0)
298          {
299            val = defaultValue;
300          }
301          else
302          {
303            val= "true".equals(propValue) ||
304              "yes".equals(propValue) || "on".equals(propValue);
305          }
306        }
307        log.debug("  " + propName + " = " + val);
308        return val;
309      }
310    
311      static
312      {
313        log.debug("... log4jdbc initializing ...");
314    
315        // look for additional driver specified in system properties
316        DebugStackPrefix = getStringOption("log4jdbc.debug.stack.prefix");
317        TraceFromApplication = DebugStackPrefix != null;
318    
319        Long thresh = getLongOption("log4jdbc.sqltiming.warn.threshold");
320        SqlTimingWarnThresholdEnabled = (thresh != null);
321        if (SqlTimingWarnThresholdEnabled)
322        {
323          SqlTimingWarnThresholdMsec = thresh.longValue();
324        }
325    
326        thresh = getLongOption("log4jdbc.sqltiming.error.threshold");
327        SqlTimingErrorThresholdEnabled = (thresh != null);
328        if (SqlTimingErrorThresholdEnabled)
329        {
330          SqlTimingErrorThresholdMsec = thresh.longValue();
331        }
332    
333        DumpBooleanAsTrueFalse =
334          getBooleanOption("log4jdbc.dump.booleanastruefalse",false);
335    
336        DumpSqlMaxLineLength = getLongOption("log4jdbc.dump.sql.maxlinelength", 90L).
337          intValue();
338    
339        DumpFullDebugStackTrace =
340          getBooleanOption("log4jdbc.dump.fulldebugstacktrace",false);
341    
342        DumpSqlSelect = getBooleanOption("log4jdbc.dump.sql.select",true);
343        DumpSqlInsert = getBooleanOption("log4jdbc.dump.sql.insert",true);
344        DumpSqlUpdate = getBooleanOption("log4jdbc.dump.sql.update",true);
345        DumpSqlDelete = getBooleanOption("log4jdbc.dump.sql.delete",true);
346        DumpSqlCreate = getBooleanOption("log4jdbc.dump.sql.create",true);
347    
348        DumpSqlFilteringOn = !(DumpSqlSelect && DumpSqlInsert && DumpSqlUpdate &&
349          DumpSqlDelete && DumpSqlCreate);
350    
351        DumpSqlAddSemicolon = getBooleanOption("log4jdbc.dump.sql.addsemicolon",false);
352    
353        // The Set of drivers that the log4jdbc driver will preload at instantiation
354        // time.  The driver can spy on any driver type, it's just a little bit
355        // easier to configure log4jdbc if it's one of these types!
356    
357        Set subDrivers = new TreeSet();
358        subDrivers.add("oracle.jdbc.driver.OracleDriver");
359        subDrivers.add("com.sybase.jdbc2.jdbc.SybDriver");
360        subDrivers.add("net.sourceforge.jtds.jdbc.Driver");
361    
362        // MS driver for Sql Server 2000
363        subDrivers.add("com.microsoft.jdbc.sqlserver.SQLServerDriver");
364    
365        // MS driver for Sql Server 2005
366        subDrivers.add("com.microsoft.sqlserver.jdbc.SQLServerDriver");
367    
368        subDrivers.add("weblogic.jdbc.sqlserver.SQLServerDriver");
369        subDrivers.add("com.informix.jdbc.IfxDriver");
370        subDrivers.add("org.apache.derby.jdbc.ClientDriver");
371        subDrivers.add("org.apache.derby.jdbc.EmbeddedDriver");
372        subDrivers.add("com.mysql.jdbc.Driver");
373        subDrivers.add("org.postgresql.Driver");
374        subDrivers.add("org.hsqldb.jdbcDriver");
375        subDrivers.add("org.h2.Driver");
376    
377        // look for additional driver specified in system properties
378        String moreDrivers = getStringOption("log4jdbc.drivers");
379    
380        if (moreDrivers != null)
381        {
382          String[] moreDriversArr = moreDrivers.split(",");
383    
384          for (int i = 0; i < moreDriversArr.length; i++)
385          {
386            subDrivers.add(moreDriversArr[i]);
387            log.debug ("    will look for additional driver " + moreDriversArr[i]);
388          }
389        }
390    
391        try
392        {
393          DriverManager.registerDriver(new DriverSpy());
394        }
395        catch (SQLException s)
396        {
397          // this exception should never be thrown, JDBC just defines it
398          // for completeness
399          throw (RuntimeException) new RuntimeException
400            ("could not register log4jdbc driver!").initCause(s);
401        }
402    
403        // instantiate all the supported drivers and remove
404        // those not found
405        String driverClass;
406        for (Iterator i = subDrivers.iterator(); i.hasNext();)
407        {
408          driverClass = (String) i.next();
409          try
410          {
411            Class.forName(driverClass);
412            log.debug("  FOUND DRIVER " + driverClass);
413          }
414          catch (Throwable c)
415          {
416            i.remove();
417          }
418        }
419    
420        if (subDrivers.size() == 0)
421        {
422          log.debug("WARNING!  " +
423            "log4jdbc couldn't find any underlying jdbc drivers.");
424        }
425    
426        SqlServerRdbmsSpecifics sqlServer = new SqlServerRdbmsSpecifics();
427    
428        /** create lookup Map for specific rdbms formatters */
429        rdbmsSpecifics = new HashMap();
430        rdbmsSpecifics.put("oracle.jdbc.driver.OracleDriver",
431          new OracleRdbmsSpecifics());
432        rdbmsSpecifics.put("net.sourceforge.jtds.jdbc.Driver", sqlServer);
433        rdbmsSpecifics.put("com.microsoft.jdbc.sqlserver.SQLServerDriver",
434          sqlServer);
435        rdbmsSpecifics.put("weblogic.jdbc.sqlserver.SQLServerDriver", sqlServer);
436    
437        log.debug("... log4jdbc initialized! ...");
438      }
439    
440      static RdbmsSpecifics defaultRdbmsSpecifics = new RdbmsSpecifics();
441    
442      /**
443       * Get the RdbmsSpecifics object for a given Connection.
444       *
445       * @param conn JDBC connection to get RdbmsSpecifics for.
446       * @return RdbmsSpecifics for the given connection.
447       */
448      static RdbmsSpecifics getRdbmsSpecifics(Connection conn)
449      {
450        String driverName = "";
451        try
452        {
453          DatabaseMetaData dbm = conn.getMetaData();
454          driverName = dbm.getDriverName();
455        }
456        catch (SQLException s)
457        {
458          // silently fail
459        }
460    
461        log.debug("driver name is " + driverName);
462    
463        RdbmsSpecifics r = (RdbmsSpecifics) rdbmsSpecifics.get(driverName);
464    
465        if (r == null)
466        {
467          return defaultRdbmsSpecifics;
468        }
469        else
470        {
471          return r;
472        }
473      }
474    
475      /**
476       * Default constructor.
477       */
478      public DriverSpy()
479      {
480      }
481    
482      /**
483       * Get the major version of the driver.  This call will be delegated to the
484       * underlying driver that is being spied upon (if there is no underlying
485       * driver found, then 1 will be returned.)
486       *
487       * @return the major version of the JDBC driver.
488       */
489      public int getMajorVersion()
490      {
491        if (lastUnderlyingDriverRequested == null)
492        {
493          return 1;
494        }
495        else
496        {
497          return lastUnderlyingDriverRequested.getMajorVersion();
498        }
499      }
500    
501      /**
502       * Get the minor version of the driver.  This call will be delegated to the
503       * underlying driver that is being spied upon (if there is no underlying
504       * driver found, then 0 will be returned.)
505       *
506       * @return the minor version of the JDBC driver.
507       */
508      public int getMinorVersion()
509      {
510        if (lastUnderlyingDriverRequested == null)
511        {
512          return 0;
513        }
514        else
515        {
516          return lastUnderlyingDriverRequested.getMinorVersion();
517        }
518      }
519    
520      /**
521       * Report whether the underlying driver is JDBC compliant.  If there is no
522       * underlying driver, false will be returned, because the driver cannot
523       * actually do any work without an underlying driver.
524       *
525       * @return <code>true</code> if the underlying driver is JDBC Compliant;
526       *         <code>false</code> otherwise.
527       */
528      public boolean jdbcCompliant()
529      {
530        return lastUnderlyingDriverRequested != null &&
531          lastUnderlyingDriverRequested.jdbcCompliant();
532      }
533    
534      /**
535       * Returns true if this is a <code>jdbc:log4</code> URL and if the URL is for
536       * an underlying driver that this DriverSpy can spy on.
537       *
538       * @param url JDBC URL.
539       *
540       * @return true if this Driver can handle the URL.
541       *
542       * @throws SQLException if a database access error occurs
543       */
544      public boolean acceptsURL(String url) throws SQLException
545      {
546        Driver d = getUnderlyingDriver(url);
547        if (d != null)
548        {
549          lastUnderlyingDriverRequested = d;
550          return true;
551        }
552        else
553        {
554          return false;
555        }
556      }
557    
558      /**
559       * Given a <code>jdbc:log4</code> type URL, find the underlying real driver
560       * that accepts the URL.
561       *
562       * @param url JDBC connection URL.
563       *
564       * @return Underlying driver for the given URL. Null is returned if the URL is
565       *         not a <code>jdbc:log4</code> type URL or there is no underlying
566       *         driver that accepts the URL.
567       *
568       * @throws SQLException if a database access error occurs.
569       */
570      private Driver getUnderlyingDriver(String url) throws SQLException
571      {
572        if (url.startsWith("jdbc:log4"))
573        {
574          url = url.substring(9);
575    
576          Enumeration e = DriverManager.getDrivers();
577    
578          Driver d;
579          while (e.hasMoreElements())
580          {
581            d = (Driver) e.nextElement();
582    
583            if (d.acceptsURL(url))
584            {
585              return d;
586            }
587          }
588        }
589        return null;
590      }
591    
592      /**
593       * Get a Connection to the database from the underlying driver that this
594       * DriverSpy is spying on.  If logging is not enabled, an actual Connection to
595       * the database returned.  If logging is enabled, a ConnectionSpy object which
596       * wraps the real Connection is returned.
597       *
598       * @param url  JDBC connection URL
599       * .
600       * @param info a list of arbitrary string tag/value pairs as
601       *             connection arguments. Normally at least a "user" and
602       *             "password" property should be included.
603       *
604       * @return     a <code>Connection</code> object that represents a
605       *             connection to the URL.
606       *
607       * @throws SQLException if a database access error occurs
608       */
609      public Connection connect(String url, Properties info) throws SQLException
610      {
611        Driver d = getUnderlyingDriver(url);
612        if (d == null)
613        {
614          return null;
615        }
616    
617        // get actual URL that the real driver expects
618        // (strip off "jdbc:log4" from url)
619        url = url.substring(9);
620    
621        lastUnderlyingDriverRequested = d;
622        Connection c = d.connect(url, info);
623    
624        if (c == null)
625        {
626          throw new SQLException("invalid or unknown driver url: " + url);
627        }
628        if (log.isJdbcLoggingEnabled())
629        {
630          ConnectionSpy cspy = new ConnectionSpy(c);
631          RdbmsSpecifics r = null;
632          String dclass = d.getClass().getName();
633          if (dclass != null && dclass.length() > 0)
634          {
635            r = (RdbmsSpecifics) rdbmsSpecifics.get(dclass);
636          }
637    
638          if (r == null)
639          {
640            r = defaultRdbmsSpecifics;
641          }
642          cspy.setRdbmsSpecifics(r);
643          return cspy;
644        }
645        else
646        {
647          return c;
648        }
649      }
650    
651      /**
652       * Gets information about the possible properties for the underlying driver.
653       *
654       * @param url  the URL of the database to which to connect
655       *
656       * @param info a proposed list of tag/value pairs that will be sent on
657       *             connect open
658       * @return     an array of <code>DriverPropertyInfo</code> objects describing
659       *             possible properties.  This array may be an empty array if no
660       *             properties are required.
661       *
662       * @throws SQLException if a database access error occurs
663       */
664      public DriverPropertyInfo[] getPropertyInfo(String url, Properties info)
665        throws SQLException
666      {
667        Driver d = getUnderlyingDriver(url);
668        if (d == null)
669        {
670          return new DriverPropertyInfo[0];
671        }
672    
673        lastUnderlyingDriverRequested = d;
674        return d.getPropertyInfo(url, info);
675      }
676    }