/*
* 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.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* ASL 2.0 and offer limited warranties, support, maintenance, and commercial
* database integrations.
*
* For more information, please visit: http://www.jooq.org/licenses
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.impl;
import static java.util.Collections.nCopies;
import static org.jooq.impl.DSL.field;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.Tools.EMPTY_FIELD;
import static org.jooq.impl.Tools.getAnnotatedGetter;
import static org.jooq.impl.Tools.getAnnotatedMembers;
import static org.jooq.impl.Tools.getAnnotatedSetters;
import static org.jooq.impl.Tools.getMatchingGetter;
import static org.jooq.impl.Tools.getMatchingMembers;
import static org.jooq.impl.Tools.getMatchingSetters;
import static org.jooq.impl.Tools.getPropertyName;
import static org.jooq.impl.Tools.hasColumnAnnotations;
import static org.jooq.tools.reflect.Reflect.accessible;
import java.beans.ConstructorProperties;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Stream;
import javax.persistence.Column;
import org.jooq.Attachable;
import org.jooq.AttachableInternal;
import org.jooq.Configuration;
import org.jooq.Field;
import org.jooq.Record;
import org.jooq.Record1;
import org.jooq.RecordMapper;
import org.jooq.RecordMapperProvider;
import org.jooq.RecordType;
import org.jooq.exception.MappingException;
import org.jooq.tools.Convert;
import org.jooq.tools.StringUtils;
import org.jooq.tools.reflect.Reflect;
/**
* This is the default implementation for <code>RecordMapper</code> types.
* <p>
* The mapping algorithm is this:
* <p>
* <h5>If <code><E></code> is an array type:</h5>
* <p>
* The resulting array is of the nature described in {@link Record#intoArray()}.
* Arrays more specific than <code>Object[]</code> can be specified as well,
* e.g. <code>String[]</code>. If conversion to the element type of more
* specific arrays fails, a {@link MappingException} is thrown, wrapping
* conversion exceptions.
* <p>
* <h5>If <code><E></code> is a field "value type" and <code><R></code>
* has exactly one column:</h5>
* <p>
* Any Java type available from {@link SQLDataType} qualifies as a well-known
* "value type" that can be converted from a single-field {@link Record1}. The
* following rules apply:
* <p>
* <ul>
* <li>If <code><E></code> is a reference type like {@link String},
* {@link Integer}, {@link Long}, {@link Timestamp}, etc., then converting from
* <code><R></code> to <code><E></code> is mere convenience for calling
* {@link Record#getValue(int, Class)} with <code>fieldIndex = 0</code></li>
* <li>If <code><E></code> is a primitive type, the mapping result will be
* the corresponding wrapper type. <code>null</code> will map to the primitive
* type's initialisation value, e.g. <code>0</code> for <code>int</code>,
* <code>0.0</code> for <code>double</code>, <code>false</code> for
* <code>boolean</code>.</li>
* </ul>
* <h5>If a default constructor is available and any JPA {@link Column}
* annotations are found on the provided <code><E></code>, only those are
* used:</h5>
* <p>
* <ul>
* <li>If <code><E></code> contains single-argument instance methods of any
* visibility annotated with <code>Column</code>, those methods are invoked</li>
* <li>If <code><E></code> contains no-argument instance methods of any
* visibility starting with <code>getXXX</code> or <code>isXXX</code>, annotated
* with <code>Column</code>, then matching <code>setXXX()</code> instance
* methods of any visibility are invoked</li>
* <li>If <code><E></code> contains instance member fields of any visibility
* annotated with <code>Column</code>, those members are set</li>
* </ul>
* Additional rules:
* <ul>
* <li>The same annotation can be re-used for several methods/members</li>
* <li>{@link Column#name()} must match {@link Field#getName()}. All other
* annotation attributes are ignored</li>
* <li>Static methods / member fields are ignored</li>
* <li>Final member fields are ignored</li>
* </ul>
* <p>
* <h5>If a default constructor is available and if there are no JPA
* <code>Column</code> annotations, or jOOQ can't find the
* <code>javax.persistence</code> API on the classpath, jOOQ will map
* <code>Record</code> values by naming convention:</h5>
* <p>
* If {@link Field#getName()} is <code>MY_field</code> (case-sensitive!), then
* this field's value will be set on all of these (regardless of visibility):
* <ul>
* <li>Single-argument instance method <code>MY_field(...)</code></li>
* <li>Single-argument instance method <code>myField(...)</code></li>
* <li>Single-argument instance method <code>setMY_field(...)</code></li>
* <li>Single-argument instance method <code>setMyField(...)</code></li>
* <li>Non-final instance member field <code>MY_field</code></li>
* <li>Non-final instance member field <code>myField</code></li>
* </ul>
* <p>
* If {@link Field#getName()} is <code>MY_field.MY_nested_field</code>
* (case-sensitive!), then this field's value will be considered a nested value
* <code>MY_nested_field</code>, which is set on a nested POJO that is passed to
* all of these (regardless of visibility):
* <ul>
* <li>Single-argument instance method <code>MY_field(...)</code></li>
* <li>Single-argument instance method <code>myField(...)</code></li>
* <li>Single-argument instance method <code>setMY_field(...)</code></li>
* <li>Single-argument instance method <code>setMyField(...)</code></li>
* <li>Non-final instance member field <code>MY_field</code></li>
* <li>Non-final instance member field <code>myField</code></li>
* </ul>
* <p>
* <h5>If no default constructor is available, but at least one constructor
* annotated with <code>ConstructorProperties</code> is available, that one is
* used</h5>
* <p>
* <ul>
* <li>The standard JavaBeans {@link ConstructorProperties} annotation is used
* to match constructor arguments against POJO members or getters.</li>
* <li>If the property names provided to the constructor match the record's
* columns via the aforementioned naming conventions, that information is used.
* </li>
* <li>If those POJO members or getters have JPA annotations, those will be used
* according to the aforementioned rules, in order to map <code>Record</code>
* values onto constructor arguments.</li>
* <li>If those POJO members or getters don't have JPA annotations, the
* aforementioned naming conventions will be used, in order to map
* <code>Record</code> values onto constructor arguments.</li>
* <li>When several annotated constructors are found, the first one is chosen,
* randomly.</li>
* <li>When invoking the annotated constructor, values are converted onto
* constructor argument types</li>
* </ul>
* <p>
* <h5>If no default constructor is available, but at least one "matching"
* constructor is available, that one is used</h5>
* <p>
* <ul>
* <li>A "matching" constructor is one with exactly as many arguments as this
* record holds fields</li>
* <li>When several "matching" constructors are found, the first one is chosen
* (as reported by {@link Class#getDeclaredConstructors()}</li>
* <li>When invoking the "matching" constructor, values are converted onto
* constructor argument types</li>
* </ul>
* <p>
* <h5>If the supplied type is an interface or an abstract class</h5>
* <p>
* Abstract types are instantiated using Java reflection {@link Proxy}
* mechanisms. The returned proxy will wrap a {@link HashMap} containing
* properties mapped by getters and setters of the supplied type. Methods (even
* JPA-annotated ones) other than standard POJO getters and setters are not
* supported. Details can be seen in {@link Reflect#as(Class)}.
* <p>
* <h5>Other restrictions</h5>
* <p>
* <ul>
* <li><code><E></code> must provide a default or a "matching" constructor.
* Non-public default constructors are made accessible using
* {@link Constructor#setAccessible(boolean)}</li>
* <li>primitive types are supported. If a value is <code>null</code>, this will
* result in setting the primitive type's default value (zero for numbers, or
* <code>false</code> for booleans). Hence, there is no way of distinguishing
* <code>null</code> and <code>0</code> in that case.</li>
* </ul>
* <p>
* This mapper is returned by the {@link DefaultRecordMapperProvider}. You can
* override this behaviour by specifying your own custom
* {@link RecordMapperProvider} in {@link Configuration#recordMapperProvider()}
*
* @author Lukas Eder
* @see RecordMapper
* @see DefaultRecordMapperProvider
* @see Configuration
*/
@SuppressWarnings("unchecked")
public class DefaultRecordMapper<R extends Record, E> implements RecordMapper<R, E> {
/**
* The record type.
*/
private final Field<?>[] fields;
private final RecordType<R> rowType;
/**
* The target type.
*/
private final Class<? extends E> type;
/**
* The configuration in whose context this {@link RecordMapper} operates.
* <p>
* This configuration can be used for caching reflection information.
*/
private final Configuration configuration;
/**
* A delegate mapper created from type information in <code>type</code>.
*/
private RecordMapper<R, E> delegate;
public DefaultRecordMapper(RecordType<R> rowType, Class<? extends E> type) {
this(rowType, type, null, null);
}
DefaultRecordMapper(RecordType<R> rowType, Class<? extends E> type, Configuration configuration) {
this(rowType, type, null, configuration);
}
DefaultRecordMapper(RecordType<R> rowType, Class<? extends E> type, E instance) {
this(rowType, type, instance, null);
}
DefaultRecordMapper(RecordType<R> rowType, Class<? extends E> type, E instance, Configuration configuration) {
this.rowType = rowType;
this.fields = rowType.fields();
this.type = type;
this.configuration = configuration;
init(instance);
}
private final void init(E instance) {
// Arrays can be mapped easily
if (type.isArray()) {
delegate = new ArrayMapper(instance);
return;
}
if (Stream.class.isAssignableFrom(type)) {
DefaultRecordMapper<R, Object[]> local = new DefaultRecordMapper<>(rowType, Object[].class, configuration);
delegate = r -> (E) Stream.of(local.map(r));
return;
}
// [#3212] [#5154] "Value types" can be mapped from single-field Record1
// types for convenience
if (type.isPrimitive()
|| DefaultDataType.types().contains(type)
|| Enum.class.isAssignableFrom(type)) {
delegate = new ValueTypeMapper();
return;
}
// [#1470] Return a proxy if the supplied type is an interface
if (Modifier.isAbstract(type.getModifiers())) {
delegate = new ProxyMapper();
return;
}
// [#2989] [#2836] Records are mapped
if (AbstractRecord.class.isAssignableFrom(type)) {
delegate = (RecordMapper<R, E>) new RecordToRecordMapper();
return;
}
// [#1340] Allow for using non-public default constructors
try {
delegate = new MutablePOJOMapper(type.getDeclaredConstructor(), instance);
return;
}
catch (NoSuchMethodException ignore) {}
// [#1336] If no default constructor is present, check if there is a
// "matching" constructor with the same number of fields as this record
Constructor<E>[] constructors = (Constructor<E>[]) type.getDeclaredConstructors();
// [#1837] If any java.beans.ConstructorProperties annotations are
// present use those rather than matching constructors by the number of
// arguments
for (Constructor<E> constructor : constructors) {
ConstructorProperties properties = constructor.getAnnotation(ConstructorProperties.class);
if (properties != null) {
delegate = new ImmutablePOJOMapperWithConstructorProperties(constructor, properties);
return;
}
}
// [#1837] Without ConstructorProperties, match constructors by matching
// argument length
for (Constructor<E> constructor : constructors) {
Class<?>[] parameterTypes = constructor.getParameterTypes();
// Match the first constructor by parameter length
if (parameterTypes.length == fields.length) {
delegate = new ImmutablePOJOMapper(constructor, parameterTypes);
return;
}
}
throw new MappingException("No matching constructor found on type " + type + " for record " + this);
}
@Override
public final E map(R record) {
if (record == null) {
return null;
}
try {
return attach(delegate.map(record), record);
}
// Pass MappingExceptions on to client code
catch (MappingException e) {
throw e;
}
// All other reflection exceptions are intercepted
catch (Exception e) {
throw new MappingException("An error ocurred when mapping record to " + type, e);
}
}
/**
* Convert a record into an array of a given type.
* <p>
* The supplied type is usually <code>Object[]</code>, but in some cases, it
* may make sense to supply <code>String[]</code>, <code>Integer[]</code>
* etc.
*/
private class ArrayMapper implements RecordMapper<R, E> {
private final E instance;
ArrayMapper(E instance) {
this.instance = instance;
}
@Override
public final E map(R record) {
int size = record.size();
Class<?> componentType = type.getComponentType();
Object[] result = (Object[]) (instance != null
? instance
: Array.newInstance(componentType, size));
// Just as in Collection.toArray(Object[]), return a new array in case
// sizes don't match
if (size > result.length) {
result = (Object[]) Array.newInstance(componentType, size);
}
for (int i = 0; i < size; i++) {
result[i] = Convert.convert(record.get(i), componentType);
}
return (E) result;
}
}
private class ValueTypeMapper implements RecordMapper<R, E> {
@Override
public final E map(R record) {
int size = record.size();
if (size != 1)
throw new MappingException("Cannot map multi-column record of degree " + size + " to value type " + type);
return record.get(0, type);
}
}
/**
* Convert a record into an hash map proxy of a given type.
* <p>
* This is done for types that are not instanciable
*/
private class ProxyMapper implements RecordMapper<R, E> {
Constructor<Lookup> constructor;
@Override
public final E map(R record) {
return new MutablePOJOMapper(null, proxy()).map(record);
}
private E proxy() {
final Object[] result = new Object[1];
final Map<String, Object> map = new HashMap<String, Object>();
final InvocationHandler handler = new InvocationHandler() {
@SuppressWarnings("null")
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
String name = method.getName();
int length = (args == null ? 0 : args.length);
if (length == 0 && name.startsWith("get"))
return map.get(name.substring(3));
else if (length == 0 && name.startsWith("is"))
return map.get(name.substring(2));
else if (length == 1 && name.startsWith("set"))
map.put(name.substring(3), args[0]);
// [#5442] Default methods should be invoked to run client implementation
else if (method.isDefault())
try {
if (constructor == null)
constructor = accessible(Lookup.class.getDeclaredConstructor(Class.class, int.class));
Class<?> declaringClass = method.getDeclaringClass();
return constructor
.newInstance(declaringClass, Lookup.PRIVATE)
.unreflectSpecial(method, declaringClass)
.bindTo(result[0])
.invokeWithArguments(args);
}
catch (Throwable e) {
throw new MappingException("Cannot invoke default method", e);
}
return null;
}
};
result[0] = Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, handler);
return (E) result[0];
}
}
/**
* Convert a record into another record type.
*/
private class RecordToRecordMapper implements RecordMapper<R, AbstractRecord> {
@Override
public final AbstractRecord map(R record) {
try {
if (record instanceof AbstractRecord) {
return ((AbstractRecord) record).intoRecord((Class<AbstractRecord>) type);
}
throw new MappingException("Cannot map record " + record + " to type " + type);
}
catch (Exception e) {
throw new MappingException("An error ocurred when mapping record to " + type, e);
}
}
}
/**
* A mapper that keeps only fields with a certain prefix prior to applying a
* delegate mapper.
*/
private class RemovingPrefixRecordMapper implements RecordMapper<R, Object> {
private final RecordMapper<R, Object> d;
private final Field<?>[] f;
RemovingPrefixRecordMapper(RecordMapper<R, Object> d, Field<?>[] fields, String prefix) {
this.d = d;
this.f = new Field[fields.length];
String dotted = prefix + ".";
for (int i = 0; i < fields.length; i++)
if (fields[i].getName().startsWith(dotted))
f[i] = field(name(fields[i].getName().substring(dotted.length() + 1)), fields[i].getDataType());
}
@Override
public Object map(R record) {
AbstractRecord copy = (AbstractRecord) DSL.using(configuration).newRecord(f);
for (int i = 0; i < f.length; i++)
if (f[i] != null)
copy.set(i, record.get(i));
return d.map(record);
}
}
/**
* Convert a record into a mutable POJO type
* <p>
* jOOQ's understanding of a mutable POJO is a Java type that has a default
* constructor
*/
private class MutablePOJOMapper implements RecordMapper<R, E> {
private final Constructor<? extends E> constructor;
private final boolean useAnnotations;
private final List<java.lang.reflect.Field>[] members;
private final List<java.lang.reflect.Method>[] methods;
private final Map<String, List<RecordMapper<R, Object>>> nested;
private final E instance;
MutablePOJOMapper(Constructor<? extends E> constructor, E instance) {
this.constructor = accessible(constructor);
this.useAnnotations = hasColumnAnnotations(configuration, type);
this.members = new List[fields.length];
this.methods = new List[fields.length];
this.nested = new HashMap<String, List<RecordMapper<R, Object>>>();
this.instance = instance;
Map<String, Field<?>[]> nestedFields = new HashMap<String, Field<?>[]>();
for (int i = 0; i < fields.length; i++) {
Field<?> field = fields[i];
String name = field.getName();
// Annotations are available and present
if (useAnnotations) {
members[i] = getAnnotatedMembers(configuration, type, name);
methods[i] = getAnnotatedSetters(configuration, type, name);
}
// No annotations are present
else {
int dot = name.indexOf('.');
// A nested mapping is applied
if (dot > -1) {
String prefix = name.substring(0, dot);
Field<?>[] f = nestedFields.get(prefix);
if (f == null) {
f = nCopies(fields.length, field("")).toArray(EMPTY_FIELD);
nestedFields.put(prefix, f);
}
f[i] = field(name(name.substring(prefix.length() + 1)), field.getDataType());
members[i] = Collections.emptyList();
methods[i] = Collections.emptyList();
}
// A top-level mapping is applied
else {
members[i] = getMatchingMembers(configuration, type, name);
methods[i] = getMatchingSetters(configuration, type, name);
}
}
}
for (Entry<String, Field<?>[]> entry : nestedFields.entrySet()) {
String prefix = entry.getKey();
List<RecordMapper<R, Object>> list = new ArrayList<RecordMapper<R, Object>>();
for (java.lang.reflect.Field member : getMatchingMembers(configuration, type, prefix)) {
list.add(new RemovingPrefixRecordMapper(
new DefaultRecordMapper<R, Object>(
new Fields<R>(entry.getValue()),
member.getType(),
null,
configuration
), fields, prefix
));
}
for (Method method : getMatchingSetters(configuration, type, prefix)) {
list.add(new RemovingPrefixRecordMapper(
new DefaultRecordMapper<R, Object>(
new Fields<R>(entry.getValue()),
method.getParameterTypes()[0],
null,
configuration
), fields, prefix
));
}
nested.put(prefix, list);
}
}
@SuppressWarnings("rawtypes")
@Override
public final E map(R record) {
try {
E result = instance != null ? instance : constructor.newInstance();
for (int i = 0; i < fields.length; i++) {
for (java.lang.reflect.Field member : members[i]) {
// [#935] Avoid setting final fields
if ((member.getModifiers() & Modifier.FINAL) == 0) {
map(record, result, member, i);
}
}
for (java.lang.reflect.Method method : methods[i]) {
Class<?> mType = method.getParameterTypes()[0];
Object value = record.get(i, mType);
// [#3082] Map nested collection types
if (value instanceof Collection && List.class.isAssignableFrom(mType)) {
Class componentType = (Class) ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
method.invoke(result, Convert.convert((Collection) value, componentType));
}
// Default reference types (including arrays)
else {
method.invoke(result, record.get(i, mType));
}
}
}
for (Entry<String, List<RecordMapper<R, Object>>> entry : nested.entrySet()) {
String prefix = entry.getKey();
for (RecordMapper<R, Object> mapper : entry.getValue()) {
Object value = mapper.map(record);
for (java.lang.reflect.Field member : getMatchingMembers(configuration, type, prefix)) {
// [#935] Avoid setting final fields
if ((member.getModifiers() & Modifier.FINAL) == 0) {
map(value, result, member);
}
}
for (Method method : getMatchingSetters(configuration, type, prefix)) {
method.invoke(result, value);
}
}
}
return result;
}
catch (Exception e) {
throw new MappingException("An error ocurred when mapping record to " + type, e);
}
}
@SuppressWarnings("rawtypes")
private final void map(Record record, Object result, java.lang.reflect.Field member, int index) throws IllegalAccessException {
Class<?> mType = member.getType();
if (mType.isPrimitive()) {
if (mType == byte.class) {
map(record.get(index, byte.class), result, member);
}
else if (mType == short.class) {
map(record.get(index, short.class), result, member);
}
else if (mType == int.class) {
map(record.get(index, int.class), result, member);
}
else if (mType == long.class) {
map(record.get(index, long.class), result, member);
}
else if (mType == float.class) {
map(record.get(index, float.class), result, member);
}
else if (mType == double.class) {
map(record.get(index, double.class), result, member);
}
else if (mType == boolean.class) {
map(record.get(index, boolean.class), result, member);
}
else if (mType == char.class) {
map(record.get(index, char.class), result, member);
}
}
else {
Object value = record.get(index, mType);
// [#3082] Map nested collection types
if (value instanceof Collection && List.class.isAssignableFrom(mType)) {
Class componentType = (Class) ((ParameterizedType) member.getGenericType()).getActualTypeArguments()[0];
member.set(result, Convert.convert((Collection) value, componentType));
}
// Default reference types (including arrays)
else {
map(value, result, member);
}
}
}
private final void map(Object value, Object result, java.lang.reflect.Field member) throws IllegalAccessException {
Class<?> mType = member.getType();
if (mType.isPrimitive()) {
if (mType == byte.class) {
member.setByte(result, (Byte) value);
}
else if (mType == short.class) {
member.setShort(result, (Short) value);
}
else if (mType == int.class) {
member.setInt(result, (Integer) value);
}
else if (mType == long.class) {
member.setLong(result, (Long) value);
}
else if (mType == float.class) {
member.setFloat(result, (Float) value);
}
else if (mType == double.class) {
member.setDouble(result, (Double) value);
}
else if (mType == boolean.class) {
member.setBoolean(result, (Boolean) value);
}
else if (mType == char.class) {
member.setChar(result, (Character) value);
}
}
else {
member.set(result, value);
}
}
}
/**
* Convert a record into an "immutable" POJO (final fields, "matching"
* constructor).
*/
private class ImmutablePOJOMapper implements RecordMapper<R, E> {
private final Constructor<E> constructor;
private final Class<?>[] parameterTypes;
public ImmutablePOJOMapper(Constructor<E> constructor, Class<?>[] parameterTypes) {
this.constructor = accessible(constructor);
this.parameterTypes = parameterTypes;
}
@Override
public final E map(R record) {
try {
Object[] converted = Convert.convert(record.intoArray(), parameterTypes);
return constructor.newInstance(converted);
}
catch (Exception e) {
throw new MappingException("An error ocurred when mapping record to " + type, e);
}
}
}
/**
* Create an immutable POJO given a constructor and its associated JavaBeans
* {@link ConstructorProperties}
*/
private class ImmutablePOJOMapperWithConstructorProperties implements RecordMapper<R, E> {
private final Constructor<E> constructor;
private final Class<?>[] parameterTypes;
private final Object[] parameterValues;
private final List<String> propertyNames;
private final boolean useAnnotations;
private final List<java.lang.reflect.Field>[] members;
private final java.lang.reflect.Method[] methods;
private final Integer[] propertyIndexes;
ImmutablePOJOMapperWithConstructorProperties(Constructor<E> constructor, ConstructorProperties properties) {
this.constructor = constructor;
this.propertyNames = Arrays.asList(properties.value());
this.useAnnotations = hasColumnAnnotations(configuration, type);
this.parameterTypes = constructor.getParameterTypes();
this.parameterValues = new Object[parameterTypes.length];
this.members = new List[fields.length];
this.methods = new Method[fields.length];
this.propertyIndexes = new Integer[fields.length];
for (int i = 0; i < fields.length; i++) {
Field<?> field = fields[i];
String name = field.getName();
String nameLC = StringUtils.toCamelCaseLC(name);
// Annotations are available and present
if (useAnnotations) {
members[i] = getAnnotatedMembers(configuration, type, name);
methods[i] = getAnnotatedGetter(configuration, type, name);
}
// No annotations are present
else {
members[i] = getMatchingMembers(configuration, type, name);
methods[i] = getMatchingGetter(configuration, type, name);
}
// [#3911] Liberal interpretation of the @ConstructorProperties specs:
// We also accept properties that don't have a matching getter or member
for (int j = 0; j < propertyNames.size(); j++) {
if (name.equals(propertyNames.get(j)) || nameLC.equals(propertyNames.get(j))) {
propertyIndexes[i] = j;
break;
}
}
}
}
@Override
public final E map(R record) {
try {
for (int i = 0; i < fields.length; i++) {
if (propertyIndexes[i] != null) {
parameterValues[propertyIndexes[i]] = record.get(i);
}
else {
for (java.lang.reflect.Field member : members[i]) {
int index = propertyNames.indexOf(member.getName());
if (index >= 0) {
parameterValues[index] = record.get(i);
}
}
if (methods[i] != null) {
String name = getPropertyName(methods[i].getName());
int index = propertyNames.indexOf(name);
if (index >= 0) {
parameterValues[index] = record.get(i);
}
}
}
}
Object[] converted = Convert.convert(parameterValues, parameterTypes);
return accessible(constructor).newInstance(converted);
}
catch (Exception e) {
throw new MappingException("An error ocurred when mapping record to " + type, e);
}
}
}
private static <E> E attach(E attachable, Record record) {
// [#2869] Attach the mapped outcome if it is Attachable and if the context's
// Settings.attachRecords flag is set
if (attachable instanceof Attachable && record instanceof AttachableInternal) {
Attachable a = (Attachable) attachable;
AttachableInternal r = (AttachableInternal) record;
if (Tools.attachRecords(r.configuration())) {
a.attach(r.configuration());
}
}
return attachable;
}
}