/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jdbi.v3.core.mapper.reflect;
import static org.jdbi.v3.core.mapper.reflect.JdbiConstructors.findConstructorFor;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.jdbi.v3.core.mapper.ColumnMapper;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.mapper.RowMapperFactory;
import org.jdbi.v3.core.statement.StatementContext;
/**
* A row mapper which maps the fields in a result set into a constructor. The default implementation will perform a
* case insensitive mapping between the constructor parameter names and the column labels,
* also considering camel-case to underscores conversion.
* <p>
* Currently the constructor must have exactly the same number of columns as the result set, and
* the mapping must be one-to-one. These restrictions may be reconsidered at a later time.
*/
public class ConstructorMapper<T> implements RowMapper<T>
{
/**
* Use the only declared constructor to map a class.
*/
public static RowMapperFactory factory(Class<?> clazz) {
return RowMapperFactory.of(clazz, ConstructorMapper.of(clazz));
}
/**
* Use the only declared constructor to map a class.
*/
public static RowMapperFactory factory(Class<?> clazz, String prefix) {
return RowMapperFactory.of(clazz, ConstructorMapper.of(clazz, prefix));
}
/**
* Use a {@code Constructor<T>} to map its declaring type.
*/
public static RowMapperFactory factory(Constructor<?> constructor) {
return RowMapperFactory.of(constructor.getDeclaringClass(), ConstructorMapper.of(constructor));
}
/**
* Use a {@code Constructor<T>} to map its declaring type.
*/
public static RowMapperFactory factory(Constructor<?> constructor, String prefix) {
return RowMapperFactory.of(constructor.getDeclaringClass(), ConstructorMapper.of(constructor, prefix));
}
/**
* Return a ConstructorMapper for the given type.
*
* @param type the mapped type
*/
public static <T> RowMapper<T> of(Class<T> type) {
return ConstructorMapper.of(findConstructorFor(type));
}
/**
* Return a ConstructorMapper for the given type and prefix
*
* @param type the mapped type
* @param prefix the column name prefix
*/
public static <T> RowMapper<T> of(Class<T> type, String prefix) {
return ConstructorMapper.of(findConstructorFor(type), prefix);
}
/**
* Return a ConstructorMapper using the given constructor
*
* @param constructor the constructor to be used in mapping
*/
public static <T> RowMapper<T> of(Constructor<T> constructor) {
return ConstructorMapper.of(constructor, DEFAULT_PREFIX);
}
/**
* Instantiate a ConstructorMapper using the given constructor and prefix
*
* @param constructor the constructor to be used in mapping
* @param prefix the column name prefix
*/
public static <T> RowMapper<T> of(Constructor<T> constructor, String prefix) {
return new ConstructorMapper<>(constructor, prefix);
}
static final String DEFAULT_PREFIX = "";
private final Constructor<T> constructor;
private final String prefix;
private ConstructorMapper(Constructor<T> constructor, String prefix) {
this.constructor = constructor;
this.prefix = prefix;
}
@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 ResultSetMetaData metadata = rs.getMetaData();
final List<String> columnNames = new ArrayList<>(metadata.getColumnCount());
for (int i = 1; i <= metadata.getColumnCount(); ++i) {
columnNames.add(metadata.getColumnLabel(i));
}
final int columns = constructor.getParameterCount();
if (columns > columnNames.size()) {
throw new IllegalStateException(columnNames.size() +
" columns in result set, but constructor takes " +
constructor.getParameterCount());
}
List<ColumnNameMatcher> columnNameMatchers = ctx.getConfig(ReflectionMappers.class).getColumnNameMatchers();
final int[] columnMap = new int[columns];
final ColumnMapper<?>[] mappers = new ColumnMapper<?>[columns];
for (int i = 0; i < columns; i++) {
final Type type = constructor.getGenericParameterTypes()[i];
final String paramName = paramName(constructor.getParameters()[i]);
final int columnIndex = columnIndexForParameter(columnNames, paramName, columnNameMatchers);
mappers[i] = ctx.findColumnMapperFor(type)
.orElseThrow(() -> new IllegalArgumentException(String.format(
"Could not find column mapper for type '%s' of parameter '%s' for constructor '%s'",
type, paramName, constructor)));
columnMap[i] = columnIndex;
}
return (r, c) -> {
final Object[] params = new Object[columns];
for (int i = 0; i < columns; i++) {
params[i] = mappers[i].map(r, columnMap[i] + 1, c);
}
try {
return constructor.newInstance(params);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
if (e.getCause() instanceof RuntimeException) {
throw (RuntimeException) e.getCause();
}
if (e.getCause() instanceof Error) {
throw (Error) e.getCause();
}
throw new RuntimeException(e);
}
};
}
private int columnIndexForParameter(List<String> columnNames,
String parameterName,
List<ColumnNameMatcher> columnNameMatchers)
{
int result = -1;
for (int i = 0; i < columnNames.size(); i++) {
String columnName = columnNames.get(i);
if (prefix.length() > 0) {
if (columnName.length() > prefix.length() &&
columnName.regionMatches(true, 0, prefix, 0, prefix.length())) {
columnName = columnName.substring(prefix.length());
}
else {
continue;
}
}
for (ColumnNameMatcher strategy : columnNameMatchers) {
if (strategy.columnNameMatches(columnName, parameterName)) {
if (result >= 0) {
throw new IllegalArgumentException(String.format(
"Constructor '%s' parameter '%s' matches multiple " +
"columns: '%s' (%d) and '%s' (%d)", constructor,
parameterName, columnNames.get(result), result,
columnNames.get(i), i));
}
result = i;
break;
}
}
}
if (result >= 0) {
return result;
}
throw new IllegalArgumentException("Constructor '" + constructor + "' parameter '" +
parameterName +
"' has no column in the result set. Verify that the Java " +
"compiler is configured to emit parameter names, " +
"that your result set has the columns expected, " +
"or annotate the parameter names explicitly with @ColumnName");
}
private static String paramName(Parameter parameter) {
ColumnName dbName = parameter.getAnnotation(ColumnName.class);
if (dbName != null) {
return dbName.value();
}
return parameter.getName();
}
}