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 }