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 }