SQLStatement.java
package org.codefilarete.stalactite.sql.statement;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import org.codefilarete.stalactite.sql.statement.binder.ParameterBinder;
import org.codefilarete.stalactite.sql.statement.binder.PreparedStatementWriter;
import org.codefilarete.stalactite.sql.statement.binder.PreparedStatementWriterIndex;
import org.codefilarete.stalactite.sql.statement.binder.PreparedStatementWriterProvider;
import org.codefilarete.tool.collection.Iterables;
/**
* Parent class that defines methods for applying values to {@link PreparedStatement} that is supposed to be built
* with the {@link #getSQL()} method. Mainly used by {@link SQLOperation} subclasses.
*
* @author Guillaume Mary
* @see SQLOperation
*/
public abstract class SQLStatement<ParamType> {
protected final Map<ParamType, Object> values = new HashMap<>(5);
protected final PreparedStatementWriterProvider<ParamType> parameterBinderProvider;
/** Set of keys/parameters/indexes available in the statement */
protected final Set<ParamType> expectedParameters;
/**
*
* @param parameterBinders expected to be the exact necessary binders of every parameter in the SQL order (no more, no less).
* Checked by {@link #assertValuesAreApplyable()}
*/
protected SQLStatement(Map<? extends ParamType, ? extends PreparedStatementWriter<?>> parameterBinders) {
this(PreparedStatementWriterIndex.fromMap(parameterBinders));
}
/**
*
* @param parameterBinderProvider expected to be the exact necessary binders of every parameter in the SQL order (no more, no less).
* Checked by {@link #assertValuesAreApplyable()}
*/
protected SQLStatement(PreparedStatementWriterIndex<? extends ParamType, ? extends PreparedStatementWriter<?>> parameterBinderProvider) {
this.parameterBinderProvider = (PreparedStatementWriterProvider<ParamType>) parameterBinderProvider;
this.expectedParameters = (Set<ParamType>) parameterBinderProvider.keys();
}
public PreparedStatementWriterIndex<ParamType, PreparedStatementWriter<?>> getParameterBinderProvider() {
return (PreparedStatementWriterIndex<ParamType, PreparedStatementWriter<?>>) parameterBinderProvider;
}
/**
* Set values to be given to the {@link PreparedStatement}. Values are not applied to {@link PreparedStatement}.
* Not expected to be called by external API
*
* @see #applyValues(PreparedStatement)
* @param values
*/
public void setValues(Map<ParamType, ?> values) {
this.values.putAll(values);
}
/**
* Set a particular value to be given to the {@link PreparedStatement}. Values are not applied to {@link PreparedStatement}.
*
* @see #applyValues(PreparedStatement)
* @param index value key in statement, usually an int or a String depending on internal statement type
* @param value value to set for index
*/
public void setValue(ParamType index, Object value) {
this.values.put(index, value);
}
/**
* @return a non-modifiable copy of values (because subclasses may not support direct modifications and it's even not encouraged by this class)
*/
public Map<ParamType, Object> getValues() {
return Collections.unmodifiableMap(values);
}
/**
* Gives original sql. Essentially used for logging
*
* @return the sql given at construction time
*/
public String getSQLSource() {
return getSQL();
}
/**
* Expected to give the SQL run in the {@link PreparedStatement}
* @return the SQL run in the {@link PreparedStatement}
*/
public abstract String getSQL();
/**
* Calls right setXXX method (according to {@link ParameterBinder} given at constructor) on the given
* {@link PreparedStatement}. Called by {@link SQLOperation} classes.
*
* @param statement target for values
*/
public void applyValues(PreparedStatement statement) {
assertValuesAreApplyable();
for (Entry<ParamType, Object> indexToValue : values.entrySet()) {
try {
doApplyValue(indexToValue.getKey(), indexToValue.getValue(), statement);
} catch (RuntimeException | OutOfMemoryError e) {
// NB: in case of BindingException it will be wrapped with some more friendly parameter name
// because the original one may use only index (see doApplyValue(..))
throw new BindingException(indexToValue.getValue(), indexToValue.getKey(), getSQL(), e);
}
}
}
public void assertValuesAreApplyable() {
Set<ParamType> paramTypes = values.keySet();
// looking for missing values
Set<ParamType> indexDiff = Iterables.minus(expectedParameters, paramTypes);
if (!indexDiff.isEmpty()) {
throw new IllegalArgumentException("Missing value for parameters " + indexDiff + " in values " + values + " in \"" + getSQL() + "\"");
}
// Looking for undefined binder
Set<ParamType> missingParameters = values.keySet().stream()
.filter(paramType -> parameterBinderProvider.doGetWriter(paramType) == null)
.collect(Collectors.toSet());
if (!missingParameters.isEmpty()) {
throw new IllegalArgumentException("Missing binder for parameters " + missingParameters + " for values " + values + " in \"" + getSQL() + "\"");
}
}
public PreparedStatementWriter<Object> getParameterBinder(ParamType parameter) {
return parameterBinderProvider.getWriter(parameter);
}
/**
* Applies a value of a parameter to a statement. Implementation is let to children classes because it depends
* on {@link ParamType} and type of value.
*
* @param key the parameter
* @param value the value of the parameter
* @param statement the statement to use
*/
protected abstract void doApplyValue(ParamType key, Object value, PreparedStatement statement);
/**
* Applies a value at an index of a statement according to a binder. Accessible from children classes.
*
* @param index the index of the parameter
* @param value the value of the parameter
* @param paramBinder the binder of the parameter on the statement
* @param statement the statement to use
*/
protected <T> void doApplyValue(int index, T value, PreparedStatementWriter<T> paramBinder, PreparedStatement statement) {
try {
paramBinder.set(statement, index, value);
} catch (SQLException e) {
throw new BindingException(value, index, getSQL(), e);
}
}
public static class BindingException extends RuntimeException {
public BindingException(String message) {
super(message);
}
public BindingException(String message, Throwable cause) {
super(message, cause);
}
public BindingException(Object value, Object paramId, String sql) {
this("Error while setting value " + value + " for parameter " + paramId + " on statement " + sql);
}
public BindingException(Object value, Object paramId, String sql, Throwable cause) {
this(value, paramId, sql);
initCause(cause);
}
}
}