package org.jdbi.v3.core.mapper.reflect;
import java.beans.ConstructorProperties;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import org.jdbi.v3.core.mapper.Nested;
import org.jdbi.v3.core.mapper.PropagateNull;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.mapper.RowMapperFactory;
import org.jdbi.v3.core.mapper.SingleColumnMapper;
import org.jdbi.v3.core.mapper.reflect.internal.PojoMapper;
import org.jdbi.v3.core.qualifier.QualifiedType;
import org.jdbi.v3.core.qualifier.Qualifiers;
import org.jdbi.v3.core.statement.StatementContext;
import static org.jdbi.v3.core.mapper.reflect.JdbiConstructors.findFactoryFor;
import static org.jdbi.v3.core.mapper.reflect.ReflectionMapperUtil.anyColumnsStartWithPrefix;
import static org.jdbi.v3.core.mapper.reflect.ReflectionMapperUtil.findColumnIndex;
import static org.jdbi.v3.core.mapper.reflect.ReflectionMapperUtil.getColumnNames;
public class ConstructorMapper<T> implements RowMapper<T> {
private static final String DEFAULT_PREFIX = "";
private static final String UNMATCHED_CONSTRUCTOR_PARAMETERS =
"Instance factory '%s' could not match any parameter to any columns in the result set. "
+ "Verify that the Java compiler is configured to emit parameter names, "
+ "that your result set has the columns expected, annotate the "
+ "parameter names explicitly with @ColumnName, or annotate nullable parameters as @Nullable";
private static final String UNMATCHED_CONSTRUCTOR_PARAMETER =
"Instance factory '%s' parameter '%s' has no matching columns in the result set. "
+ "Verify that the Java compiler is configured to emit parameter names, "
+ "that your result set has the columns expected, annotate the "
+ "parameter names explicitly with @ColumnName, or annotate nullable parameters as @Nullable";
private static final String UNMATCHED_COLUMNS_STRICT =
"Mapping instance factory %s could not match parameters for columns: %s";
private static final String MISSING_COLUMN_MAPPER =
"Could not find column mapper for type '%s' of parameter '%s' for instance factory '%s'";
public static RowMapperFactory factory(Class<?> clazz) {
return RowMapperFactory.of(clazz, ConstructorMapper.of(clazz));
}
public static RowMapperFactory factory(Class<?> clazz, String prefix) {
return RowMapperFactory.of(clazz, ConstructorMapper.of(clazz, prefix));
}
public static RowMapperFactory factory(Constructor<?> constructor) {
return RowMapperFactory.of(constructor.getDeclaringClass(), ConstructorMapper.of(constructor));
}
public static RowMapperFactory factory(Constructor<?> constructor, String prefix) {
return RowMapperFactory.of(constructor.getDeclaringClass(), ConstructorMapper.of(constructor, prefix));
}
public static <T> RowMapper<T> of(Class<T> type) {
return ConstructorMapper.of(type, DEFAULT_PREFIX);
}
public static <T> RowMapper<T> of(Class<T> type, String prefix) {
return new ConstructorMapper<>(findFactoryFor(type), prefix);
}
public static <T> RowMapper<T> of(Constructor<T> constructor) {
return ConstructorMapper.of(constructor, DEFAULT_PREFIX);
}
public static <T> RowMapper<T> of(Constructor<T> constructor, String prefix) {
return new ConstructorMapper<>(new ConstructorInstanceFactory<>(constructor), prefix);
}
private final InstanceFactory<T> factory;
private final String prefix;
private final ConstructorProperties constructorProperties;
private final Map<Parameter, ConstructorMapper<?>> nestedMappers = new ConcurrentHashMap<>();
private ConstructorMapper(InstanceFactory<T> factory, String prefix) {
this.factory = factory;
this.prefix = prefix.toLowerCase();
this.constructorProperties = factory.getAnnotation(ConstructorProperties.class);
}
@Override
public T map(ResultSet rs, StatementContext ctx) throws SQLException {
return specialize(rs, ctx).map(rs, ctx);
}
@Override
public RowMapper<T> specialize(ResultSet rs, StatementContext ctx) throws SQLException {
final List<String> columnNames = getColumnNames(rs);
final List<ColumnNameMatcher> columnNameMatchers =
ctx.getConfig(ReflectionMappers.class).getColumnNameMatchers();
final List<String> unmatchedColumns = new ArrayList<>(columnNames);
RowMapper<T> mapper = specialize0(ctx, columnNames, columnNameMatchers, unmatchedColumns)
.orElseThrow(() -> new IllegalArgumentException(String.format(
UNMATCHED_CONSTRUCTOR_PARAMETERS, factory)));
if (ctx.getConfig(ReflectionMappers.class).isStrictMatching()
&& anyColumnsStartWithPrefix(unmatchedColumns, prefix, columnNameMatchers)) {
throw new IllegalArgumentException(
String.format(UNMATCHED_COLUMNS_STRICT, factory, unmatchedColumns));
}
return mapper;
}
private Optional<RowMapper<T>> specialize0(StatementContext ctx,
List<String> columnNames,
List<ColumnNameMatcher> columnNameMatchers,
List<String> unmatchedColumns) {
final int count = factory.getParameterCount();
final Parameter[] parameters = factory.getParameters();
boolean matchedColumns = false;
final List<String> unmatchedParameters = new ArrayList<>();
final List<ParameterData> paramData = new ArrayList<>();
for (int i = 0; i < count; i++) {
final Parameter parameter = parameters[i];
boolean nullable = isNullable(parameter);
Nested anno = parameter.getAnnotation(Nested.class);
if (anno == null) {
final String paramName = prefix + paramName(parameters, i, constructorProperties);
final OptionalInt columnIndex = findColumnIndex(paramName, columnNames, columnNameMatchers,
() -> debugName(parameter));
if (columnIndex.isPresent()) {
int colIndex = columnIndex.getAsInt();
final QualifiedType<?> type = QualifiedType.of(parameter.getParameterizedType())
.withAnnotations(ctx.getConfig(Qualifiers.class).findFor(parameter));
paramData.add(new ParameterData(i, parameter, ctx.findColumnMapperFor(type)
.map(mapper -> new SingleColumnMapper<>(mapper, colIndex + 1))
.orElseThrow(() -> new IllegalArgumentException(
String.format(MISSING_COLUMN_MAPPER, type, paramName, factory)))));
matchedColumns = true;
unmatchedColumns.remove(columnNames.get(colIndex));
} else if (nullable) {
paramData.add(new ParameterData(i, parameter, (r, c) -> null));
} else {
unmatchedParameters.add(paramName);
}
} else {
final String nestedPrefix = prefix + anno.value();
final Optional<? extends RowMapper<?>> nestedMapper = nestedMappers
.computeIfAbsent(parameter, p ->
new ConstructorMapper<>(findFactoryFor(p.getType()), nestedPrefix))
.specialize0(ctx, columnNames, columnNameMatchers, unmatchedColumns);
if (nestedMapper.isPresent()) {
paramData.add(new ParameterData(i, parameter, nestedMapper.get()));
matchedColumns = true;
} else if (nullable) {
paramData.add(new ParameterData(i, parameter, (r, c) -> null));
} else {
unmatchedParameters.add(paramName(parameters, i, constructorProperties));
}
}
}
if (!matchedColumns) {
return Optional.empty();
}
Collections.sort(paramData, Comparator.comparing(
p -> p.propagateNull ? 1 : 0));
if (!unmatchedParameters.isEmpty()) {
throw new IllegalArgumentException(String.format(
UNMATCHED_CONSTRUCTOR_PARAMETER, factory, unmatchedParameters));
}
final Optional<String> nullMarkerColumn =
Optional.ofNullable(factory.getAnnotationIncludingType(PropagateNull.class))
.map(PropagateNull::value);
return Optional.of((r, c) -> {
if (PojoMapper.propagateNull(r, nullMarkerColumn)) {
return null;
}
final Object[] params = new Object[count];
for (ParameterData p : paramData) {
params[p.index] = p.mapper.map(r, c);
if (p.propagateNull && (params[p.index] == null || p.isPrimitive && r.wasNull())) {
return null;
}
}
return factory.newInstance(params);
});
}
private boolean isNullable(Parameter parameter) {
return Stream.of(parameter.getAnnotations())
.map(Annotation::annotationType)
.map(Class::getSimpleName)
.anyMatch("Nullable"::equals);
}
private static String paramName(Parameter[] parameters,
int position,
ConstructorProperties parameterNames) {
final Parameter parameter = parameters[position];
ColumnName dbName = parameter.getAnnotation(ColumnName.class);
if (dbName != null) {
return dbName.value();
}
if (parameterNames != null) {
return parameterNames.value()[position];
}
return parameter.getName();
}
private String debugName(Parameter parameter) {
return String.format("%s constructor parameter %s",
factory.getDeclaringClass().getSimpleName(),
parameter.getName());
}
private static class ParameterData {
ParameterData(int index, Parameter parameter, RowMapper<?> mapper) {
this.index = index;
this.parameter = parameter;
this.mapper = mapper;
propagateNull = parameter.getAnnotation(PropagateNull.class) != null;
isPrimitive = parameter.getType().isPrimitive();
}
final int index;
final Parameter parameter;
final RowMapper<?> mapper;
final boolean propagateNull;
final boolean isPrimitive;
}
}