package rocks.inspectit.agent.java.sensor.method.jdbc;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
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.ThreadLocalStack;
/**
* Stores the mapping between statements and objects so that these statements are later accessible.
*
* @author Patrice Bouillet
* @author Stefan Siegl
*/
@Component
public class StatementStorage {
/**
* The logger of this class. Initialized manually.
*/
private static final Logger LOG = LoggerFactory.getLogger(StatementStorage.class);
/** representation of a null value. */
private static final String NULL_VALUE = "null";
/**
* This cache keeps track of the prepared statement objects and associates these with the
* concrete query string and its the bound parameters. Weak keys ensure that elements will be
* cleared regularly. As the key is the PreparedStatement object, as soon as the application
* looses the reference to this object (which is usually very quickly after the invocation), the
* garbage collector can remove this object and thus the entry in the cache. In order to
* safe-guard our cache, elements will also be removed if they are not used for 20 minutes.
*
* <b> Note that this data structure provides atomic access like a <code>ConcurrentMap</code>.
* </b>.
*/
private Cache<Object, QueryInformation> preparedStatements = CacheBuilder.newBuilder().expireAfterAccess(20 * 60, TimeUnit.SECONDS).weakKeys().build();
/**
* Returns the sql thread local stack.
*/
private ThreadLocalStack<String> sqlThreadLocalStack = new ThreadLocalStack<String>();
/**
* Adds a prepared statement to this storage for later retrieval.
*
* @param object
* The object which will be the key of the mapping.
*/
public void addPreparedStatement(Object object) {
String sql = sqlThreadLocalStack.getLast();
preparedStatements.put(object, new QueryInformation(sql));
if (LOG.isDebugEnabled()) {
LOG.debug("Recorded prepared sql statement: " + sql);
}
}
/**
* Returns a stored sql string for this object.
*
* @param object
* The object which will be used to look up in the map.
* @return The sql string or <code>null</code> if this statement is not available within the
* storage.
*/
protected String getPreparedStatement(Object object) {
QueryInformation queryAndParameters = preparedStatements.getIfPresent(object);
String query = null;
if (null != queryAndParameters) {
query = queryAndParameters.query;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Return prepared sql statement: " + query);
}
return query;
}
/**
* Returns a stored parameters for the object.
*
* @param object
* The object which will be used to look up in the map.
* @return The list of parameters or <code> null </code> if there is no container for the given
* SQL statement or there are no parameters captured within this SQL statement.
*/
protected List<String> getParameters(Object object) {
QueryInformation queryAndParameters = preparedStatements.getIfPresent(object);
if (null == queryAndParameters) {
return null;
} else {
return queryAndParameters.getParametersAsList();
}
}
/**
* Adds a parameter to a specific prepared statement.
*
* @param preparedStatement
* The prepared statement object.
* @param index
* The index of the value.
* @param value
* The value to be inserted.
*/
protected void addParameter(Object preparedStatement, int index, Object value) {
QueryInformation queryAndParameters = preparedStatements.getIfPresent(preparedStatement);
if (null == queryAndParameters) {
if (LOG.isDebugEnabled()) {
LOG.debug("Could not get the prepared statement from the cache to add a parameter! Prepared Statement:" + preparedStatement + " index:" + index + " value:" + value);
}
return;
}
String[] parameters = queryAndParameters.getParameters();
if ((0 > index) || (parameters.length <= index)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Trying to set the parameter with value " + value + " at index " + index + ", but the prepared statement did not have parameter on this index.");
}
return;
}
if (null != value) {
if ((value instanceof String) || (value instanceof Date) || (value instanceof Time) || (value instanceof Timestamp)) {
parameters[index] = "'" + value.toString() + "'";
} else {
parameters[index] = value.toString();
}
} else {
value = NULL_VALUE;
parameters[index] = (String) value;
}
if (LOG.isDebugEnabled()) {
LOG.debug("Prepared Statement :: Added value:" + value.toString() + " with index:" + index + " to prepared statement:" + preparedStatement);
}
}
/**
* Clears all the parameters in the array.
*
* @param preparedStatement
* The prepared statement for which all parameters are going to be cleared.
*/
protected void clearParameters(Object preparedStatement) {
QueryInformation queryAndParameters = preparedStatements.getIfPresent(preparedStatement);
if (null == queryAndParameters) {
if (LOG.isDebugEnabled()) {
LOG.debug("Could not get the prepared statement from the cache to clear the parameters! Prepared Statement:" + preparedStatement);
}
return;
}
queryAndParameters.clearParameters();
}
/**
* This method adds an SQL String to the current thread local stack. This is needed so that
* created prepared statements can be associated to the SQL Strings.
* <p>
* So if three times the prepared statement method is called with the same string, the stack
* contains the string three times. Now the Prepared Statement is created which results in
* calling the {@link #addPreparedStatement(Object, String)} method. The last added String is
* taken and associated with the object.
*
* @param sql
* The SQL String.
*/
protected void addSql(String sql) {
sqlThreadLocalStack.push(sql);
}
/**
* Removes the last added sql from the thread local stack. We don't need the String object here.
*/
protected void removeSql() {
sqlThreadLocalStack.pop();
}
/**
* Value container to store the SQL query and its parameters within the cache of prepared
* statements. The JDBC sensor in inspectIT allows for two modes. The SQL query can be enhanced
* with the values of the bind parameters. This happens if and only if "SQL Prepared Statement
* Parameter Replacement" is set. Thus this container ensures that this calculation is only done
* if this is needed, that is when the parameters are first accessed. Thus ensure that you only
* access the parameters if you really want to fill them!
*
* <p>
* To access the parameters during the "filling stage", prefer the
* <code> public String[] getParameters() </code> method as this allows to access the internal
* String[].
*
* @author Stefan Siegl
*/
private static class QueryInformation {
/** the SQL query. */
private String query;
/**
* internal container of the SQL bind values. The size of this array defines the number of
* bind values. This field is filled on first access.
*/
private String[] parameters = null;
/**
* Creates a new instance of this value container. Please note that creating an instance of
* this class does not calculate the number of available bind values (a.k.a. "parameters").
* They are creating on first use.
*
* @param query
* The SQL query.
*/
public QueryInformation(String query) {
this.query = query;
}
/**
* Returns the String[] representation of the bind values of this SQL query. This method
* should be used over the <code>List<String> getParametersAsList</code> method to fill the
* parameters as this method provides access to the backing String[] and is thus more
* efficient.
*
* <b> please note that the calculation of the number of parameters within the SQL query is
* done with the first access to this method. Thus only call this method if you know that
* you do have parameters to set, else there will be unnecessary calculations. </b>
*
* @return <code>String[]</code> containing the current bind values of this SQL query. The
* size of the array can be used to deduce the number of available bind parameters
* based on the SQL query.
*/
public String[] getParameters() {
if (null == parameters) {
// Calculate the amount of parameters based on the SQL query. We calculate this
// value on first request as this is only needed if we have parameter capturing
// active.
int count = 0;
for (int i = query.length() - 1; i > 0; i--) {
if (query.charAt(i) == '?') {
count++;
}
}
parameters = new String[count];
}
return parameters; // NOPMD: no copy to improve performance
}
/**
* Resets the bind parameters of this SQL query.
*
* <b> please note that the calculation of the number of parameters within the SQL query
* will be done as a side-effect of this method (but only if it is not already done before).
* Thus only call this method if you know that you do have parameters to set, else there
* will be unnecessary calculations. </b>
*/
public void clearParameters() {
if (null == parameters) {
return;
}
parameters = new String[parameters.length];
}
/**
* Returns the parameter values as <code>List<String></code>. The list will provide a
* representation of the parameters. Adding to this list will <b> not </b> change the
* parameters. This method is meant to be used from the second after hook to report the
* current parameters.
* <p>
* Please note that to fill the parameters the String[] should be used.
*
* @return the parameter values as <code>List<String></code> or <code> null </code> if no
* parameters are captured.
*/
public List<String> getParametersAsList() {
if (null == parameters) {
return null;
} else {
return Arrays.asList(parameters);
}
}
}
}