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 }