package rocks.inspectit.agent.java.sensor.method.jdbc;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import rocks.inspectit.agent.java.util.ReflectionCache;
import rocks.inspectit.shared.all.communication.data.SqlStatementData;
/**
* Storage for the meta information of JDBC connection classes.
*
* @author Stefan Siegl
*/
@Component
public class ConnectionMetaDataStorage {
/**
* Empty server as marker that a meta data can not be created for connection.
*/
protected static final ConnectionMetaData EMPTY = new ConnectionMetaData();
/**
* This cache keeps track of the meta information for connection objects. The key is the <code>
* Connection </code> object. As connections are re-used it makes sense to keep the meta
* information cached, as to not request this information over and over again.
*
* Also we use weak keys as to allow the garbage collector to remove the entries as soon as
* nobody else holds references to the connection (if this happens, the connection itself is
* garbage collected and thus we need not hold meta information anymore).
*
* Also note that having weakKeys tells the Cache to use identity comparison (==) instead of
* equals() comparison. This is just the thing we want as we can ensure that connections
* identity stays that same. On top of that == is faster than equals.
*
* <b> Note that this data structure provides atomic access like a <code>ConcurrentMap</code>.
* </b>.
*
* Package access for easier testing.
*/
Cache<Object, ConnectionMetaData> storage = CacheBuilder.newBuilder().weakKeys().softValues().build();
/**
* Extractor to read data from the connection instance.
*
* Package access for easier testing.
*/
ConnectionMetaDataExtractor dataExtractor = new ConnectionMetaDataExtractor();
/**
* Populates the given SQL Statement data with the meta information from the storage if this
* data exist.
*
* @param sqlData
* the data object to populate.
* @param connection
* the connection.
*/
public void populate(SqlStatementData sqlData, Object connection) {
ConnectionMetaData connectionMetaData = get(connection);
if ((null != connectionMetaData) && (EMPTY != connectionMetaData)) { // NOPMD == on purpose
sqlData.setDatabaseProductName(connectionMetaData.product);
sqlData.setDatabaseProductVersion(connectionMetaData.version);
sqlData.setDatabaseUrl(connectionMetaData.url);
}
}
/**
* Retrieves the <code>ConnectionMetaData</code> stored with this connection.
*
* @param connection
* the connection instance
* @return the <code>ConnectionMetaData</code> stored with this connection.
*/
private ConnectionMetaData get(final Object connection) {
if (null == connection) {
return null;
}
try {
return storage.get(connection, new Callable<ConnectionMetaData>() {
@Override
public ConnectionMetaData call() throws Exception {
ConnectionMetaData data = dataExtractor.parse(connection);
return data != null ? data : EMPTY;
}
});
} catch (ExecutionException e) {
// should not occur as we have no checked exceptions
return EMPTY;
}
}
/**
* Value holder for meta information of connection instances.
*
* @author Stefan Siegl
*/
public static class ConnectionMetaData {
/** The connection URL. */
public String url; // NOCHK
/** The product name of the database. */
public String product; // NOCHK
/** The version of the database. */
public String version; // NOCHK
}
/**
* Extractor to retrieve connection meta data information. This class uses reflection to get the
* information from the connection. To ensure high performance it caches the reflection
* <code>Method</code> objects using the {@link ReflectionCache}.
*
* @author Stefan Siegl
*/
static class ConnectionMetaDataExtractor {
/** FQN of the java.sql.DatabaseMetaData. */
private static final String JAVA_SQL_DATABASE_META_DATA_FQN = "java.sql.DatabaseMetaData";
/** FQN of the java.sql.Connection. */
private static final String JAVA_SQL_CONNECTION_FQN = "java.sql.Connection";
/** Method names. */
private static final String GET_META_DATA = "getMetaData";
/** Method names. */
private static final String GET_URL = "getURL";
/** Method names. */
private static final String GET_DATABASE_PRODUCT_VERSION = "getDatabaseProductVersion";
/** Method names. */
private static final String GET_DATABASE_PRODUCT_NAME = "getDatabaseProductName";
/** Method names. */
private static final String IS_CLOSED = "isClosed";
/** Extractor for the JDBC URL. */
static JDBCUrlExtractor urlExtractor = new JDBCUrlExtractor();
/** Cache for the <code> Method </code> elements. */
static ReflectionCache cache = new ReflectionCache();
/**
* The logger of this class. Initialized manually.
*/
static Logger logger = LoggerFactory.getLogger(ConnectionMetaDataExtractor.class);
/**
* Parses a given <code>Connection</code> and retrieves the monitoring-related meta
* information.
*
* @param connection
* the <code>Connection</code> object.
* @return meta information about the connection for monitoring. returns <code>null</code>
* in case connection is <code>null</code> or connection is closed.
*/
public ConnectionMetaData parse(Object connection) {
if (null == connection) {
logger.warn("Meta Information on database cannot be read for the null connection.");
return null;
}
Class<?> connectionClass = connection.getClass();
if (isClosed(connectionClass, connection)) {
if (logger.isDebugEnabled()) {
logger.debug("Meta Information on database cannot be read because the connection is closed.");
}
return null;
}
ConnectionMetaData data = new ConnectionMetaData();
Object metaData = getMetaData(connectionClass, connection);
if (null == metaData) {
logger.warn("Meta information on database cannot be read for connection " + connection.toString() + ". No database details like URL or Vendor will be displayed.");
return data;
}
Class<?> metaDataClass = metaData.getClass();
data.version = parseVersion(metaDataClass, metaData);
data.url = parseTarget(metaDataClass, metaData);
data.product = parseProduct(metaDataClass, metaData);
return data;
}
/**
* Checks if the connection is closed.
*
* @param connectionClass
* the connection class.
* @param connection
* the connection instance.
* @return the result of calling isClosed on the connection object or <code>true</code> any
* exception occurs during method invocation
*/
private boolean isClosed(Class<?> connectionClass, Object connection) {
return (Boolean) cache.invokeMethod(connectionClass, IS_CLOSED, null, connection, null, true, JAVA_SQL_CONNECTION_FQN);
}
/**
* Retrieves the meta information object from the connection.
*
* @param connectionClass
* the connection class.
* @param connection
* the connection instance.
* @return the meta information object from the connection or <code>null</code> in case of
* problems.
*/
private Object getMetaData(Class<?> connectionClass, Object connection) {
return cache.invokeMethod(connectionClass, GET_META_DATA, null, connection, null, null, JAVA_SQL_CONNECTION_FQN);
}
/**
* Retrieves the target/url from the jdbc connection string.
*
* @param databaseMetaDataClass
* the meta information class.
* @param databaseMetaData
* the meta information object of the connection.
* @return the target/url from the jdbc connection string.
*/
private String parseTarget(Class<?> databaseMetaDataClass, Object databaseMetaData) {
String url = (String) cache.invokeMethod(databaseMetaDataClass, GET_URL, null, databaseMetaData, null, null, JAVA_SQL_DATABASE_META_DATA_FQN);
return urlExtractor.extractURLfromJDBCURL(url);
}
/**
* Retrieves the version of the database.
*
* @param databaseMetaDataClass
* the meta information class.
* @param databaseMetaData
* the meta information instance of the connection.
* @return the version of the database.
*/
private String parseVersion(Class<?> databaseMetaDataClass, Object databaseMetaData) {
return (String) cache.invokeMethod(databaseMetaDataClass, GET_DATABASE_PRODUCT_VERSION, null, databaseMetaData, null, null, JAVA_SQL_DATABASE_META_DATA_FQN);
}
/**
* Retrieves the product name of the database.
*
* @param databaseMetaDataClass
* the meta information class.
* @param databaseMetaData
* the meta information instance of the connection.
* @return the product name of the database.
*/
private String parseProduct(Class<?> databaseMetaDataClass, Object databaseMetaData) {
return (String) cache.invokeMethod(databaseMetaDataClass, GET_DATABASE_PRODUCT_NAME, null, databaseMetaData, null, null, JAVA_SQL_DATABASE_META_DATA_FQN);
}
}
/**
* Extractor to retrieve the concrete URL from the JDBC connection string.
*
* @author Stefan Siegl
*/
static class JDBCUrlExtractor {
/**
* URL pattern to read jdbc URL from jdbc connection string.
* jdbc:sqlserver://[serverName[\instanceName
* ][:portNumber]][;property=value[;property=value]]
* jdbc:db2://<HOST>:<PORT>/<DATABASE_NAME> --> remove the //
* jdbc:h2:../../database/database/dvdstore22
*
* Oracle is once again different: http://www.orafaq.com/wiki/JDBC
* "jdbc:oracle:thin:@//myhost:1521/orcl"; "jdbc:oracle:thin:@myhost:1521:orcl";
* "jdbc:oracle:oci:@myhost:1521:orcl";
*
* use: http://www.regexr.com/ to play around with regex. See
* http://www.regular-expressions.info/named.html as great reference.
*/
private final Pattern urlPattern = Pattern.compile("^jdbc:(?:oracle:.*?|.*?):(?:[@/]*)?(.*?)([;?].*)?$");
/**
* Extracts the url from the connection string.
*
* @param url
* the connection string
* @return the url.
*/
public String extractURLfromJDBCURL(String url) {
if (StringUtils.isEmpty(url)) {
return "";
}
try {
final Matcher matcher = urlPattern.matcher(url);
matcher.find();
return matcher.group(1);
} catch (IllegalStateException i) {
return url;
}
}
}
}