# 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);
    		}
    	}
    }