SQLOperation.java
package org.codefilarete.stalactite.sql.statement;
import javax.annotation.Nullable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.codefilarete.stalactite.sql.ConnectionProvider;
import org.codefilarete.stalactite.sql.statement.SQLStatement.BindingException;
import org.codefilarete.tool.bean.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tries to simplify usage of {@link PreparedStatement} in oriented scenario like:
* - set values on {@link PreparedStatement}
* - executeBatch {@link PreparedStatement} (see {@link WriteOperation})
*
* Logging of SQL execution can be activated with a logger with this class name.
* If you want more fine-grained logs, SQL statements can be logged with DEBUG level, whereas values can be logged with TRACE level.
* <b>Despite that activation of fined grained logs defers by level, they are always logged at DEBUG level.</b> (which is not really consistent).
*
* @see WriteOperation
* @see ReadOperation
* @param <ParamType> type of sqlStatement value entries, for example String (for {@link StringParamedSQL}), Integer (for {@link PreparedSQL}
*
* @author Guillaume Mary
*/
public abstract class SQLOperation<ParamType> implements AutoCloseable {
/** Made public for internal project usage, not aimed at being used outside */
public static final Logger LOGGER = LoggerFactory.getLogger(SQLOperation.class);
/** Listener that does nothing, made to prevent from not-null-matching if */
public static final SQLOperationListener NOOP_LISTENER = new SQLOperationListener() {
/* Expected to do nothing, so we do nothing */
};
protected final ConnectionProvider connectionProvider;
protected PreparedStatement preparedStatement;
protected final SQLStatement<ParamType> sqlStatement;
private SQLOperationListener<ParamType> listener = NOOP_LISTENER;
private String sql;
/** Parameters that mustn't be logged for security reason for instance */
private Set<ParamType> notLoggedParams = Collections.emptySet();
/** Timeout for SQL orders, default is null meaning that JDBC default timeout applies, which is generally 0, which means no timeout */
private Integer timeout = null;
/**
* Constructor with mandatory parameters
* @param sqlStatement the statement that must be executed by this operation
* @param connectionProvider JDBC {@link Connection} provider that will be used to get a connection to execute the statement on
*/
public SQLOperation(SQLStatement<ParamType> sqlStatement, ConnectionProvider connectionProvider) {
this.sqlStatement = sqlStatement;
this.connectionProvider = connectionProvider;
}
public ConnectionProvider getConnectionProvider() {
return connectionProvider;
}
public SQLStatement<ParamType> getSqlStatement() {
return sqlStatement;
}
public SQLOperationListener<ParamType> getListener() {
return listener;
}
public void setListener(@Nullable SQLOperationListener<ParamType> listener) {
this.listener = Objects.preventNull(listener, NOOP_LISTENER);
}
/**
* Simple wrapping over {@link SQLStatement#setValues(Map)}
* @param values values for each parameter
*/
public void setValues(Map<ParamType, ?> values) {
this.listener.onValuesSet(values);
// we transfer data to our own structure
this.sqlStatement.setValues(values);
}
/**
* Simple wrapping over {@link SQLStatement#setValue(Object, Object)}
*
* @param index parameter/index for which value mumst be set
* @param value parameter/index value
*/
public void setValue(ParamType index, Object value) {
this.listener.onValueSet(index, value);
this.sqlStatement.setValue(index, value);
}
/**
* Common operation for subclasses. Rebuild PreparedStatement if connection has changed. Call {@link #getSQL()} when
* necessary.
*/
protected void ensureStatement() {
try {
Connection connection = this.connectionProvider.giveConnection();
if (this.preparedStatement == null) {
prepareStatement(connection);
}
} catch (RuntimeException | SQLException e) {
throw new BindingException("Error while creating statement " + getSQL(), e);
}
}
/**
* Expected to create the internal field {@link #preparedStatement} from given {@link Connection}
* and {@link SQLStatement#getSQL()} result.
* Exposed to eventually adapt the created {@link PreparedStatement}.
*
* @param connection the {@link Connection} to use to create the {@link PreparedStatement}}
* @throws SQLException in case of error during {@link PreparedStatement} creation from the given {@link Connection}
*/
protected void prepareStatement(Connection connection) throws SQLException {
this.preparedStatement = connection.prepareStatement(getSQL());
}
/**
* Gives the SQL that is used in the {@link PreparedStatement}.
* Called by {@link SQLStatement#getSQL()} then stores the result.
*
* @return the SQL that is used in the {@link PreparedStatement}
*/
protected String getSQL() {
if (this.sql == null) {
this.sql = sqlStatement.getSQL();
}
return this.sql;
}
protected void prepareExecute() {
listener.onExecute(sqlStatement);
applyValuesToEnsuredStatement();
logExecution();
try {
applyTimeout();
} catch (SQLException e) {
throw new SQLExecutionException(getSQL(), e);
}
}
protected void applyValuesToEnsuredStatement() {
ensureStatement();
try {
this.sqlStatement.applyValues(preparedStatement);
} catch (RuntimeException e) {
throw new BindingException("Error while applying values " + this.sqlStatement.values + " on statement " + getSQL(), e);
}
}
/**
* Cancels the underlying {@link PreparedStatement} (if exists and not closed, to avoid unnecessary exceptions)
*
* @throws SQLException this of the {@link PreparedStatement#cancel()} method
*/
public void cancel() throws SQLException {
if (this.preparedStatement != null) {
this.preparedStatement.cancel();
}
}
/**
* Gives current {@link PreparedStatement}
*
* @return current {@link PreparedStatement}, maybe null, closed, cancel ...
*/
@Nullable
public PreparedStatement getPreparedStatement() {
return preparedStatement;
}
/**
*
* @return null means default timeout applies, else the timeout set, 0 means infinite (see JDBC specification)
*/
public Integer getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
/**
* Closes the internal {@link PreparedStatement}
*/
@Override
public void close() {
try {
if (preparedStatement != null) {
this.preparedStatement.close();
}
} catch (SQLException e) {
LOGGER.warn("Can't close statement properly", e);
}
}
/**
* Set params that mustn't be logged when debug is activated for values
* @param notLoggedParams set of not loggable values
*/
public void setNotLoggedParams(Set<ParamType> notLoggedParams) {
this.notLoggedParams = notLoggedParams;
}
protected Map<ParamType, Object> filterLoggable(Map<ParamType, ?> values) {
// we make a copy of values to prevent alteration
Map<ParamType, Object> loggedValues = new HashMap<>(values);
loggedValues.entrySet().forEach(e -> {
if (notLoggedParams.contains(e.getKey())) {
// we change logged value so param is still present in mapped params, showing that it hasn't disappeared
e.setValue("X-masked value-X");
}
});
return loggedValues;
}
protected void applyTimeout() throws SQLException {
if (getTimeout() != null) {
this.preparedStatement.setQueryTimeout(getTimeout());
}
}
protected void logExecution() {
logExecution(filterLoggable(sqlStatement.getValues()).toString());
}
protected void logExecution(String printableValues) {
// we log statement only in strict DEBUG mode because it's also logged in TRACE mode and DEBUG os also active at TRACE level
if (LOGGER.isDebugEnabled() && !LOGGER.isTraceEnabled()) {
LOGGER.debug(sqlStatement.getSQLSource());
} else if (LOGGER.isTraceEnabled()) {
LOGGER.trace("{} | {}", sqlStatement.getSQLSource(), printableValues);
}
}
/**
* Contract to implement for being notified of actions on an {@link SQLOperation}
* Created for use cases where listening to SQL orders is necessary but auditing logs is not enough because you only get a String version of
* what is executed.
* <strong>Be aware that sensible values are not filtered</strong> at the opposite to logged ones hence you get same values passed to SQL
* order, DON'T LOG THEM. Hence this listener is not made to replace logging system.
*
* @param <ParamType> type of the {@link SQLOperation} to be registered on
*/
public interface SQLOperationListener<ParamType> {
/**
* Called when the {@link SQLOperation#setValues(Map)} is called.
* Please note that the given {@link Map} is writable which allow values to be modified, but this is not the primary goal on this method
* and is not an active feature and may be changed in the future. This is done so because making it unmodifiable needs a superfluous
* instantiation.
* Please note also that this behavior defers from {@link #onValueSet(Object, Object)} where the value is "readonly" : since it is passed
* by reference (Java language), simple (non complex) types are considered readonly.
*
* <strong>Be aware that sensible values are not filtered</strong> at the opposite to logged ones hence you get same values passed to SQL
* order, DON'T LOG THEM.
*
* @param values
*/
default void onValuesSet(Map<ParamType, ?> values) {
// does nothing by default
}
/**
* Called when the {@link SQLOperation#setValue(Object, Object)} is called.
* <strong>Be aware that sensible values are not filtered</strong> at the opposite to logged ones hence you get same values passed to SQL
* order, DON'T LOG THEM.
*
* @param param
* @param value
*/
default void onValueSet(ParamType param, Object value) {
// does nothing by default
}
default void onExecute(SQLStatement<ParamType> sqlStatement) {
}
}
}