ResultSetReader.java
package org.codefilarete.stalactite.sql.statement.binder;
import java.lang.invoke.MethodHandleInfo;
import java.lang.invoke.SerializedLambda;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.function.Function;
import org.codefilarete.reflection.MethodReferenceCapturer.MethodDefinition;
import org.codefilarete.stalactite.sql.statement.SQLStatement.BindingException;
import org.codefilarete.tool.Strings;
import org.codefilarete.tool.function.SerializableThrowingBiFunction;
import org.codefilarete.tool.function.SerializableThrowingFunction;
import org.codefilarete.tool.function.ThrowingBiFunction;
import static org.codefilarete.reflection.MethodReferenceCapturer.giveArgumentTypes;
import static org.codefilarete.reflection.MethodReferences.buildSerializedLambda;
/**
* An interface that allows to use {@link ResultSet#getXXX(...)} method to be used as method reference.
* See {@link DefaultResultSetReaders} for some available ones.
* See {@link PreparedStatementWriter} for its equivalence to write to {@link java.sql.PreparedStatement}, or
* {@link ParameterBinder} to accomplish both.
*
* @author Guillaume Mary
* @see PreparedStatementWriter
* @see DefaultResultSetReaders
*/
public interface ResultSetReader<I> extends JdbcBinder<I> {
/**
* Creates a {@link ResultSetReader} using a method reference to a {@link ResultSet} getter method.
* The type of the reader is inferred from the provided method reference.
*
* @param resultSetGetter a method reference to a {@link ResultSet} getter method (e.g., ResultSet::getString)
* @param <O> the type returned by the {@link ResultSet} getter
* @return a {@link ResultSetReader} that invokes the specified {@link ResultSet} method
*/
static <O> ResultSetReader<O> ofMethodReference(SerializableThrowingBiFunction<ResultSet, String, O, SQLException> resultSetGetter) {
Class<O> argumentType = giveArgumentTypes(buildSerializedLambda(resultSetGetter)).getReturnType();
return new LambdaResultSetReader<>(resultSetGetter, argumentType);
}
/**
* Reads column <code>columnName</code> returned by <code>resultSet</code>.
* This implementation wraps any exception in a {@link BindingException} and in particular wraps {@link ClassCastException} for better message
* handling.
* Subclasses are expected to implement {@link #doGet(ResultSet, String)}
*
* @param resultSet the {@link ResultSet} to read
* @param columnName the column to be read from the given {@link ResultSet}
* @return content of <code>columnName</code>, typed according to <code>column</code>
*/
default I get(ResultSet resultSet, String columnName) {
try {
return doGet(resultSet, columnName);
} catch (SQLException e) {
throw new BindingException("Error while reading column '" + columnName + "'", e);
} catch (ClassCastException e) {
String[] classNames = e.getMessage().split(" cannot be cast to ");
CharSequence ellipsedValue = Strings.ellipsis(String.valueOf(DefaultResultSetReaders.OBJECT_READER.get(resultSet, columnName)), 15);
throw new BindingException("Error while reading column '" + columnName + "' : trying to read '" + ellipsedValue + "' as " + classNames[1]
+ " but was " + classNames[0], e);
}
}
/**
* Method expected to be overridden for really reading {@link ResultSet} value if one want to benefit from exception handling by {@link #get(ResultSet, String)}
*
* @param resultSet the {@link ResultSet} to read
* @param columnName the column to be read from the given {@link ResultSet}
* @return content of <code>columnName</code>, typed according to <code>column</code>
* @throws SQLException the exception thrown be the underlying access to the {@link ResultSet}
*/
I doGet(ResultSet resultSet, String columnName) throws SQLException;
/**
* Builds a new {@link ResultSetReader} from this one by applying a converter on the output object
*
* @param converter the {@link Function} that turns output value to the final type
* @param <O> final type
* @return a new {@link ResultSetReader} based on this one plus a converting {@link Function}
* @see PreparedStatementWriter#preApply(SerializableThrowingFunction)
*/
default <O> ResultSetReader<O> thenApply(SerializableThrowingFunction<I, O, ? extends Throwable> converter) {
// Determining the type of next ResultSetReader: this is based
SerializedLambda methodReference = buildSerializedLambda(converter);
MethodDefinition methodDefinition = giveArgumentTypes(methodReference);
Class<O> readerType;
if (methodReference.getImplMethodKind() == MethodHandleInfo.REF_invokeStatic) {
// static method reference : its result type is the type of our new ResultSetReader
readerType = methodDefinition.getReturnType();
} else {
if (methodDefinition.getName().equals("<init>")) {
// referenced method is a constructor
readerType = methodDefinition.getDeclaringClass();
} else {
// case of an instance method reference, or a real lambda expression
readerType = methodDefinition.getReturnType();
}
}
return new LambdaResultSetReader<O>((rs, columnName) -> {
try {
return converter.apply(this.get(rs, columnName));
} catch (Throwable e) {
throw new RuntimeException(e);
}
}, readerType) {
@Override
public <U> Class<U> getColumnType() {
return ResultSetReader.this.getColumnType();
}
};
}
/**
* Class that helps to wrap a {@link ResultSet} method as a {@link ResultSetReader}
* @param <I> type read by the reader
* @author Guillaume Mary
*/
class LambdaResultSetReader<I> implements ResultSetReader<I> {
private final ThrowingBiFunction<ResultSet, String, I, SQLException> delegate;
private final Class<I> type;
public LambdaResultSetReader(ThrowingBiFunction<ResultSet, String, I, SQLException> delegate, Class<I> type) {
this.delegate = delegate;
this.type = type;
}
@Override
public Class<I> getType() {
return type;
}
@Override
public I doGet(ResultSet resultSet, String columnName) throws SQLException {
return delegate.apply(resultSet, columnName);
}
}
}