/**
* Copyright 2007-2012 Arthur Blake
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sf.log4jdbc;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
/**
* A JDBC driver which is a facade that delegates to one or more real underlying
* JDBC drivers. The driver will spy on any other JDBC driver that is loaded,
* simply by prepending <code>jdbc:log4</code> to the normal jdbc driver URL
* used by any other JDBC driver. The driver, by default, also loads several
* well known drivers at class load time, so that this driver can be
* "dropped in" to any Java program that uses these drivers without making any
* code changes.
* <p/>
* The well known driver classes that are loaded are:
* <p/>
* <p/>
* <code>
* <ul>
* <li>oracle.jdbc.driver.OracleDriver</li>
* <li>com.sybase.jdbc2.jdbc.SybDriver</li>
* <li>net.sourceforge.jtds.jdbc.Driver</li>
* <li>com.microsoft.jdbc.sqlserver.SQLServerDriver</li>
* <li>com.microsoft.sqlserver.jdbc.SQLServerDriver</li>
* <li>weblogic.jdbc.sqlserver.SQLServerDriver</li>
* <li>com.informix.jdbc.IfxDriver</li>
* <li>org.apache.derby.jdbc.ClientDriver</li>
* <li>org.apache.derby.jdbc.EmbeddedDriver</li>
* <li>com.mysql.jdbc.Driver</li>
* <li>org.postgresql.Driver</li>
* <li>org.hsqldb.jdbcDriver</li>
* <li>org.h2.Driver</li>
* </ul>
* </code>
* <p/>
* <p/>
* Additional drivers can be set via a property: <b>log4jdbc.drivers</b>
* This can be either a single driver class name or a list of comma separated
* driver class names.
* <p/>
* The autoloading behavior can be disabled by setting a property:
* <b>log4jdbc.auto.load.popular.drivers</b> to false. If that is done, then
* the only drivers that log4jdbc will attempt to load are the ones specified
* in <b>log4jdbc.drivers</b>.
* <p/>
* If any of the above driver classes cannot be loaded, the driver continues on
* without failing.
* <p/>
* Note that the <code>getMajorVersion</code>, <code>getMinorVersion</code> and
* <code>jdbcCompliant</code> method calls attempt to delegate to the last
* underlying driver requested through any other call that accepts a JDBC URL.
* <p/>
* This can cause unexpected behavior in certain circumstances. For example,
* if one of these 3 methods is called before any underlying driver has been
* established, then they will return default values that might not be correct
* in all situations. Similarly, if this spy driver is used to spy on more than
* one underlying driver concurrently, the values returned by these 3 method
* calls may change depending on what the last underlying driver used was at the
* time. This will not usually be a problem, since the driver is retrieved by
* it's URL from the DriverManager in the first place (thus establishing an
* underlying real driver), and in most applications their is only one database.
*
* @author Arthur Blake
* @author Tim Azzopardi One line change to add support for JDBC name for Oracle
*/
public class DriverSpy implements Driver
{
/**
* The last actual, underlying driver that was requested via a URL.
*/
private Driver lastUnderlyingDriverRequested;
/**
* Maps driver class names to RdbmsSpecifics objects for each kind of
* database.
*/
private static Map rdbmsSpecifics;
static final SpyLogDelegator log = SpyLogFactory.getSpyLogDelegatorDefault();
/**
* Optional package prefix to use for finding application generating point of
* SQL.
*/
static String DebugStackPrefix;
/**
* Flag to indicate debug trace info should be from the calling application
* point of view (true if DebugStackPrefix is set.)
*/
static boolean TraceFromApplication;
/**
* Flag to indicate if a warning should be shown if SQL takes more than
* SqlTimingWarnThresholdMsec milliseconds to run. See below.
*/
static boolean SqlTimingWarnThresholdEnabled;
/**
* An amount of time in milliseconds for which SQL that executed taking this
* long or more to run shall cause a warning message to be generated on the
* SQL timing logger.
*
* This threshold will <i>ONLY</i> be used if SqlTimingWarnThresholdEnabled
* is true.
*/
static long SqlTimingWarnThresholdMsec;
/**
* Flag to indicate if an error should be shown if SQL takes more than
* SqlTimingErrorThresholdMsec milliseconds to run. See below.
*/
static boolean SqlTimingErrorThresholdEnabled;
/**
* An amount of time in milliseconds for which SQL that executed taking this
* long or more to run shall cause an error message to be generated on the
* SQL timing logger.
*
* This threshold will <i>ONLY</i> be used if SqlTimingErrorThresholdEnabled
* is true.
*/
static long SqlTimingErrorThresholdMsec;
/**
* When dumping boolean values, dump them as 'true' or 'false'.
* If this option is not set, they will be dumped as 1 or 0 as many
* databases do not have a boolean type, and this allows for more
* portable sql dumping.
*/
static boolean DumpBooleanAsTrueFalse;
/**
* When dumping SQL, if this is greater than 0, than the SQL will
* be broken up into lines that are no longer than this value.
*/
static int DumpSqlMaxLineLength;
/**
* If this is true, display a special warning in the log along with the SQL
* when the application uses a Statement (as opposed to a PreparedStatement.)
* Using Statements for frequently used SQL can sometimes result in
* performance and/or security problems.
*/
static boolean StatementUsageWarn;
/**
* Options to more finely control which types of SQL statements will
* be dumped, when dumping SQL.
* By default all 5 of the following will be true. If any one is set to
* false, then that particular type of SQL will not be dumped.
*/
static boolean DumpSqlSelect;
static boolean DumpSqlInsert;
static boolean DumpSqlUpdate;
static boolean DumpSqlDelete;
static boolean DumpSqlCreate;
// only true if one ore more of the above 4 flags are false.
static boolean DumpSqlFilteringOn;
/**
* If true, add a semilcolon to the end of each SQL dump.
*/
static boolean DumpSqlAddSemicolon;
/**
* If dumping in debug mode, dump the full stack trace.
* This will result in a VERY voluminous output, but can be very useful
* under some circumstances.
*/
static boolean DumpFullDebugStackTrace;
/**
* Attempt to Automatically load a set of popular JDBC drivers?
*/
static boolean AutoLoadPopularDrivers;
/**
* Trim SQL before logging it?
*/
static boolean TrimSql;
/**
* Remove extra Lines in the SQL that consist of only white space?
* Only when 2 or more lines in a row like this occur, will the extra lines (beyond 1)
* be removed.
*/
static boolean TrimExtraBlankLinesInSql;
/**
* Coldfusion typically calls PreparedStatement.getGeneratedKeys() after
* every SQL update call, even if it's not warranted. This typically produces
* an exception that is ignored by Coldfusion. If this flag is true, then
* any exception generated by this method is also ignored by log4jdbc.
*/
static boolean SuppressGetGeneratedKeysException;
/**
* Get a Long option from a property and
* log a debug message about this.
*
* @param props Properties to get option from.
* @param propName property key.
*
* @return the value of that property key, converted
* to a Long. Or null if not defined or is invalid.
*/
private static Long getLongOption(Properties props, String propName)
{
String propValue = props.getProperty(propName);
Long longPropValue = null;
if (propValue == null)
{
log.debug("x " + propName + " is not defined");
}
else
{
try
{
longPropValue = new Long(Long.parseLong(propValue));
log.debug(" " + propName + " = " + longPropValue);
}
catch (NumberFormatException n)
{
log.debug("x " + propName + " \"" + propValue +
"\" is not a valid number");
}
}
return longPropValue;
}
/**
* Get a Long option from a property and
* log a debug message about this.
*
* @param props Properties to get option from.
* @param propName property key.
*
* @return the value of that property key, converted
* to a Long. Or null if not defined or is invalid.
*/
private static Long getLongOption(Properties props, String propName,
long defaultValue)
{
String propValue = props.getProperty(propName);
Long longPropValue;
if (propValue == null)
{
log.debug("x " + propName + " is not defined (using default of " +
defaultValue +")");
longPropValue = new Long(defaultValue);
}
else
{
try
{
longPropValue = new Long(Long.parseLong(propValue));
log.debug(" " + propName + " = " + longPropValue);
}
catch (NumberFormatException n)
{
log.debug("x " + propName + " \"" + propValue +
"\" is not a valid number (using default of " + defaultValue +")");
longPropValue = new Long(defaultValue);
}
}
return longPropValue;
}
/**
* Get a String option from a property and
* log a debug message about this.
*
* @param props Properties to get option from.
* @param propName property key.
* @return the value of that property key.
*/
private static String getStringOption(Properties props, String propName)
{
String propValue = props.getProperty(propName);
if (propValue == null || propValue.length()==0)
{
log.debug("x " + propName + " is not defined");
propValue = null; // force to null, even if empty String
}
else
{
log.debug(" " + propName + " = " + propValue);
}
return propValue;
}
/**
* Get a boolean option from a property and
* log a debug message about this.
*
* @param props Properties to get option from.
* @param propName property name to get.
* @param defaultValue default value to use if undefined.
*
* @return boolean value found in property, or defaultValue if no property
* found.
*/
private static boolean getBooleanOption(Properties props, String propName,
boolean defaultValue)
{
String propValue = props.getProperty(propName);
boolean val;
if (propValue == null)
{
log.debug("x " + propName + " is not defined (using default value " +
defaultValue + ")");
return defaultValue;
}
else
{
propValue = propValue.trim().toLowerCase();
if (propValue.length() == 0)
{
val = defaultValue;
}
else
{
val= "true".equals(propValue) ||
"yes".equals(propValue) || "on".equals(propValue);
}
}
log.debug(" " + propName + " = " + val);
return val;
}
static
{
log.debug("... log4jdbc initializing ...");
InputStream propStream =
DriverSpy.class.getResourceAsStream("/log4jdbc.properties");
Properties props = new Properties(System.getProperties());
if (propStream != null)
{
try
{
props.load(propStream);
}
catch (IOException e)
{
log.debug("ERROR! io exception loading " +
"log4jdbc.properties from classpath: " + e.getMessage());
}
finally
{
try
{
propStream.close();
}
catch (IOException e)
{
log.debug("ERROR! io exception closing property file stream: " +
e.getMessage());
}
}
log.debug(" log4jdbc.properties loaded from classpath");
}
else
{
log.debug(" log4jdbc.properties not found on classpath");
}
// look for additional driver specified in properties
DebugStackPrefix = getStringOption(props, "log4jdbc.debug.stack.prefix");
TraceFromApplication = DebugStackPrefix != null;
Long thresh = getLongOption(props, "log4jdbc.sqltiming.warn.threshold");
SqlTimingWarnThresholdEnabled = (thresh != null);
if (SqlTimingWarnThresholdEnabled)
{
SqlTimingWarnThresholdMsec = thresh.longValue();
}
thresh = getLongOption(props, "log4jdbc.sqltiming.error.threshold");
SqlTimingErrorThresholdEnabled = (thresh != null);
if (SqlTimingErrorThresholdEnabled)
{
SqlTimingErrorThresholdMsec = thresh.longValue();
}
DumpBooleanAsTrueFalse =
getBooleanOption(props, "log4jdbc.dump.booleanastruefalse",false);
DumpSqlMaxLineLength = getLongOption(props,
"log4jdbc.dump.sql.maxlinelength", 90L).intValue();
DumpFullDebugStackTrace =
getBooleanOption(props, "log4jdbc.dump.fulldebugstacktrace",false);
StatementUsageWarn =
getBooleanOption(props, "log4jdbc.statement.warn",false);
DumpSqlSelect = getBooleanOption(props, "log4jdbc.dump.sql.select",true);
DumpSqlInsert = getBooleanOption(props, "log4jdbc.dump.sql.insert",true);
DumpSqlUpdate = getBooleanOption(props, "log4jdbc.dump.sql.update",true);
DumpSqlDelete = getBooleanOption(props, "log4jdbc.dump.sql.delete",true);
DumpSqlCreate = getBooleanOption(props, "log4jdbc.dump.sql.create",true);
DumpSqlFilteringOn = !(DumpSqlSelect && DumpSqlInsert && DumpSqlUpdate &&
DumpSqlDelete && DumpSqlCreate);
DumpSqlAddSemicolon = getBooleanOption(props,
"log4jdbc.dump.sql.addsemicolon", false);
AutoLoadPopularDrivers = getBooleanOption(props,
"log4jdbc.auto.load.popular.drivers", true);
TrimSql = getBooleanOption(props, "log4jdbc.trim.sql", true);
TrimExtraBlankLinesInSql = getBooleanOption(props, "log4jdbc.trim.sql.extrablanklines", true);
SuppressGetGeneratedKeysException =
getBooleanOption(props, "log4jdbc.suppress.generated.keys.exception",
false);
// The Set of drivers that the log4jdbc driver will preload at instantiation
// time. The driver can spy on any driver type, it's just a little bit
// easier to configure log4jdbc if it's one of these types!
Set subDrivers = new TreeSet();
if (AutoLoadPopularDrivers)
{
subDrivers.add("oracle.jdbc.driver.OracleDriver");
subDrivers.add("oracle.jdbc.OracleDriver");
subDrivers.add("com.sybase.jdbc2.jdbc.SybDriver");
subDrivers.add("net.sourceforge.jtds.jdbc.Driver");
// MS driver for Sql Server 2000
subDrivers.add("com.microsoft.jdbc.sqlserver.SQLServerDriver");
// MS driver for Sql Server 2005
subDrivers.add("com.microsoft.sqlserver.jdbc.SQLServerDriver");
subDrivers.add("weblogic.jdbc.sqlserver.SQLServerDriver");
subDrivers.add("com.informix.jdbc.IfxDriver");
subDrivers.add("org.apache.derby.jdbc.ClientDriver");
subDrivers.add("org.apache.derby.jdbc.EmbeddedDriver");
subDrivers.add("com.mysql.jdbc.Driver");
subDrivers.add("org.postgresql.Driver");
subDrivers.add("org.hsqldb.jdbcDriver");
subDrivers.add("org.h2.Driver");
}
// look for additional driver specified in properties
String moreDrivers = getStringOption(props, "log4jdbc.drivers");
if (moreDrivers != null)
{
String[] moreDriversArr = moreDrivers.split(",");
for (int i = 0; i < moreDriversArr.length; i++)
{
subDrivers.add(moreDriversArr[i]);
log.debug (" will look for specific driver " + moreDriversArr[i]);
}
}
try
{
DriverManager.registerDriver(new DriverSpy());
}
catch (SQLException s)
{
// this exception should never be thrown, JDBC just defines it
// for completeness
throw (RuntimeException) new RuntimeException
("could not register log4jdbc driver!").initCause(s);
}
// instantiate all the supported drivers and remove
// those not found
String driverClass;
for (Iterator i = subDrivers.iterator(); i.hasNext();)
{
driverClass = (String) i.next();
try
{
Class.forName(driverClass);
log.debug(" FOUND DRIVER " + driverClass);
}
catch (Throwable c)
{
i.remove();
}
}
if (subDrivers.size() == 0)
{
log.debug("WARNING! " +
"log4jdbc couldn't find any underlying jdbc drivers.");
}
SqlServerRdbmsSpecifics sqlServer = new SqlServerRdbmsSpecifics();
OracleRdbmsSpecifics oracle = new OracleRdbmsSpecifics();
MySqlRdbmsSpecifics mySql = new MySqlRdbmsSpecifics();
/** create lookup Map for specific rdbms formatters */
rdbmsSpecifics = new HashMap();
rdbmsSpecifics.put("oracle.jdbc.driver.OracleDriver", oracle);
rdbmsSpecifics.put("oracle.jdbc.OracleDriver", oracle);
rdbmsSpecifics.put("Oracle JDBC driver", oracle);
rdbmsSpecifics.put("net.sourceforge.jtds.jdbc.Driver", sqlServer);
rdbmsSpecifics.put("com.microsoft.jdbc.sqlserver.SQLServerDriver",
sqlServer);
rdbmsSpecifics.put("weblogic.jdbc.sqlserver.SQLServerDriver", sqlServer);
rdbmsSpecifics.put("com.mysql.jdbc.Driver", mySql);
log.debug("... log4jdbc-remix initialized! ...");
}
static RdbmsSpecifics defaultRdbmsSpecifics = new RdbmsSpecifics();
/**
* Get the RdbmsSpecifics object for a given Connection.
*
* @param conn JDBC connection to get RdbmsSpecifics for.
* @return RdbmsSpecifics for the given connection.
*/
static RdbmsSpecifics getRdbmsSpecifics(Connection conn)
{
String driverName = "";
try
{
DatabaseMetaData dbm = conn.getMetaData();
driverName = dbm.getDriverName();
}
catch (SQLException s)
{
// silently fail
}
log.debug("driver name is " + driverName);
RdbmsSpecifics r = (RdbmsSpecifics) rdbmsSpecifics.get(driverName);
if (r == null)
{
return defaultRdbmsSpecifics;
}
else
{
return r;
}
}
/**
* Default constructor.
*/
public DriverSpy()
{
}
/**
* Get the major version of the driver. This call will be delegated to the
* underlying driver that is being spied upon (if there is no underlying
* driver found, then 1 will be returned.)
*
* @return the major version of the JDBC driver.
*/
public int getMajorVersion()
{
if (lastUnderlyingDriverRequested == null)
{
return 1;
}
else
{
return lastUnderlyingDriverRequested.getMajorVersion();
}
}
/**
* Get the minor version of the driver. This call will be delegated to the
* underlying driver that is being spied upon (if there is no underlying
* driver found, then 0 will be returned.)
*
* @return the minor version of the JDBC driver.
*/
public int getMinorVersion()
{
if (lastUnderlyingDriverRequested == null)
{
return 0;
}
else
{
return lastUnderlyingDriverRequested.getMinorVersion();
}
}
/**
* Report whether the underlying driver is JDBC compliant. If there is no
* underlying driver, false will be returned, because the driver cannot
* actually do any work without an underlying driver.
*
* @return <code>true</code> if the underlying driver is JDBC Compliant;
* <code>false</code> otherwise.
*/
public boolean jdbcCompliant()
{
return lastUnderlyingDriverRequested != null &&
lastUnderlyingDriverRequested.jdbcCompliant();
}
/**
* Returns true if this is a <code>jdbc:log4</code> URL and if the URL is for
* an underlying driver that this DriverSpy can spy on.
*
* @param url JDBC URL.
*
* @return true if this Driver can handle the URL.
*
* @throws SQLException if a database access error occurs
*/
public boolean acceptsURL(String url) throws SQLException
{
Driver d = getUnderlyingDriver(url);
if (d != null)
{
lastUnderlyingDriverRequested = d;
return true;
}
else
{
return false;
}
}
/**
* Given a <code>jdbc:log4</code> type URL, find the underlying real driver
* that accepts the URL.
*
* @param url JDBC connection URL.
*
* @return Underlying driver for the given URL. Null is returned if the URL is
* not a <code>jdbc:log4</code> type URL or there is no underlying
* driver that accepts the URL.
*
* @throws SQLException if a database access error occurs.
*/
private Driver getUnderlyingDriver(String url) throws SQLException
{
if (url.startsWith("jdbc:log4"))
{
url = url.substring(9);
Enumeration e = DriverManager.getDrivers();
Driver d;
while (e.hasMoreElements())
{
d = (Driver) e.nextElement();
if (d.acceptsURL(url))
{
return d;
}
}
}
return null;
}
/**
* Get a Connection to the database from the underlying driver that this
* DriverSpy is spying on. If logging is not enabled, an actual Connection to
* the database returned. If logging is enabled, a ConnectionSpy object which
* wraps the real Connection is returned.
*
* @param url JDBC connection URL
* .
* @param info a list of arbitrary string tag/value pairs as
* connection arguments. Normally at least a "user" and
* "password" property should be included.
*
* @return a <code>Connection</code> object that represents a
* connection to the URL.
*
* @throws SQLException if a database access error occurs
*/
public Connection connect(String url, Properties info) throws SQLException
{
Driver d = getUnderlyingDriver(url);
if (d == null)
{
return null;
}
// get actual URL that the real driver expects
// (strip off "jdbc:log4" from url)
url = url.substring(9);
lastUnderlyingDriverRequested = d;
Connection c = d.connect(url, info);
if (c == null)
{
throw new SQLException("invalid or unknown driver url: " + url);
}
if (log.isJdbcLoggingEnabled())
{
ConnectionSpy cspy = new ConnectionSpy(c, log);
RdbmsSpecifics r = null;
String dclass = d.getClass().getName();
if (dclass != null && dclass.length() > 0)
{
r = (RdbmsSpecifics) rdbmsSpecifics.get(dclass);
}
if (r == null)
{
r = defaultRdbmsSpecifics;
}
cspy.setRdbmsSpecifics(r);
return cspy;
}
else
{
return c;
}
}
/**
* Gets information about the possible properties for the underlying driver.
*
* @param url the URL of the database to which to connect
*
* @param info a proposed list of tag/value pairs that will be sent on
* connect open
* @return an array of <code>DriverPropertyInfo</code> objects describing
* possible properties. This array may be an empty array if no
* properties are required.
*
* @throws SQLException if a database access error occurs
*/
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info)
throws SQLException
{
Driver d = getUnderlyingDriver(url);
if (d == null)
{
return new DriverPropertyInfo[0];
}
lastUnderlyingDriverRequested = d;
return d.getPropertyInfo(url, info);
}
}