/*
 * 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.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
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.impl.Tools.newRecord;
import static org.jooq.impl.Tools.recordType;
import static org.jooq.impl.Tools.row0;
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.Executable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.persistence.Column;

import org.jooq.Attachable;
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.Result;
import org.jooq.Table;
import org.jooq.TableField;
import org.jooq.TableRecord;
import org.jooq.conf.Settings;
import org.jooq.exception.MappingException;
import org.jooq.tools.Convert;
import org.jooq.tools.StringUtils;
import org.jooq.tools.reflect.Reflect;
import org.jooq.tools.reflect.ReflectException;

This is the default implementation for RecordMapper types, which applies to Record.into(Class<? extends Object>), Result.into(Class), and similar calls.

The mapping algorithm is this:

If <E> is an array type:

The resulting array is of the nature described in Record.intoArray(). Arrays more specific than Object[] can be specified as well, e.g. String[]. If conversion to the element type of more specific arrays fails, a MappingException is thrown, wrapping conversion exceptions.

If <E> is a field "value type" and <R extends Record1<?>>, i.e. it has exactly one column:

Any Java type available from SQLDataType qualifies as a well-known "value type" that can be converted from a single-field Record1. The following rules apply:

  • If <E> is a reference type like String, Integer, Long, Timestamp, etc., then converting from <R> to <E> is mere convenience for calling Record.getValue(int, Class<? extends Object>) with fieldIndex = 0
  • If <E> is a primitive type, the mapping result will be the corresponding wrapper type. null will map to the primitive type's initialisation value, e.g. 0 for int, 0.0 for double, false for boolean.

If <E> is a TableRecord type (e.g. from a generated record), then its meta data are used:

Generated TableRecord types reference their corresponding generated Table types, which provide TableField meta data through TableLike.fields(). All target Record.fields() are looked up in the source table via TableLike.indexOf(Field) and their values are mapped. Excess source values and missing target values are ignored.

If a default constructor is available and any JPA Column annotations are found on the provided <E>, only those are used:

  • If <E> contains single-argument instance methods of any visibility annotated with Column, those methods are invoked
  • If <E> contains no-argument instance methods of any visibility starting with getXXX or isXXX, annotated with Column, then matching setXXX() instance methods of any visibility are invoked
  • If <E> contains instance member fields of any visibility annotated with Column, those members are set
Additional rules:
  • The same annotation can be re-used for several methods/members
  • name.name() must match Field.getName(). All other annotation attributes are ignored
  • Static methods / member fields are ignored
  • Final member fields are ignored

If a default constructor is available and if there are no JPA Column annotations, or jOOQ can't find the javax.persistence API on the classpath, jOOQ will map Record values by naming convention:

If Field.getName() is MY_field (case-sensitive!), then this field's value will be set on all of these (regardless of visibility):

  • Single-argument instance method MY_field(...)
  • Single-argument instance method myField(...)
  • Single-argument instance method setMY_field(...)
  • Single-argument instance method setMyField(...)
  • Non-final instance member field MY_field
  • Non-final instance member field myField

If Field.getName() is MY_field.MY_nested_field (case-sensitive!), then this field's value will be considered a nested value MY_nested_field, which is set on a nested POJO that is passed to all of these (regardless of visibility):

  • Single-argument instance method MY_field(...)
  • Single-argument instance method myField(...)
  • Single-argument instance method setMY_field(...)
  • Single-argument instance method setMyField(...)
  • Non-final instance member field MY_field
  • Non-final instance member field myField

If no default constructor is available, but at least one constructor annotated with ConstructorProperties is available, that one is used

  • The standard JavaBeans ConstructorProperties annotation is used to match constructor arguments against POJO members or getters.
  • If the property names provided to the constructor match the record's columns via the aforementioned naming conventions, that information is used.
  • If those POJO members or getters have JPA annotations, those will be used according to the aforementioned rules, in order to map Record values onto constructor arguments.
  • If those POJO members or getters don't have JPA annotations, the aforementioned naming conventions will be used, in order to map Record values onto constructor arguments.
  • When several annotated constructors are found, the first one is chosen, randomly.
  • When invoking the annotated constructor, values are converted onto constructor argument types

If Kotlin is available and the argument class has Kotlin reflection meta data available, and Settings.isMapConstructorParameterNamesInKotlin() is turned on, parameter names are reflected and used.

  • The Kotlin compiler adds meta data available for reflection using Kotlin reflection APIs to derive parameter names.

If no default constructor is available, but at least one "matching" constructor is available, that one is used

  • A "matching" constructor is one with exactly as many arguments as this record holds fields
  • When several "matching" constructors are found, the first one is chosen (as reported by Class.getDeclaredConstructors()). This choice is non-deterministic as neither the JVM nor the JDK guarantee any order of methods or constructors.
  • When Settings.isMapConstructorParameterNames() is turned on, and parameter names are available through reflection on Executable.getParameters(), then values are mapped by name, otherwise by index. (see #4627)
  • When invoking the "matching" constructor, values are converted onto constructor argument types

If no default constructor is available, no "matching" constructor is available, but Settings.isMapConstructorParameterNames() is turned on, and parameter names are available through reflection on Executable.getParameters(), the first constructor is used

  • The first constructor is chosen (as reported by Class.getDeclaredConstructors()). This choice is non-deterministic as neither the JVM nor the JDK guarantee any order of methods or constructors.
  • When invoking that constructor, values are converted onto constructor argument types

If the supplied type is an interface or an abstract class

Abstract types are instantiated using Java reflection Proxy mechanisms. The returned proxy will wrap a 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 Reflect.as(Class<Object>).

Other restrictions

  • <E> must provide a default or a "matching" constructor. Non-public default constructors are made accessible using AccessibleObject.setAccessible(boolean)
  • primitive types are supported. If a value is null, this will result in setting the primitive type's default value (zero for numbers, or false for booleans). Hence, there is no way of distinguishing null and 0 in that case.

This mapper is returned by the DefaultRecordMapperProvider. You can override this behaviour by specifying your own custom RecordMapperProvider in Configuration.recordMapperProvider()

Author:Lukas Eder
See Also:
/** * This is the default implementation for <code>RecordMapper</code> types, which * applies to {@link Record#into(Class)}, {@link Result#into(Class)}, and * similar calls. * <p> * The mapping algorithm is this: * <p> * <h5>If <code>&lt;E&gt;</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>&lt;E&gt;</code> is a field "value type" and * <code>&lt;R extends Record1&lt;?&gt;&gt;</code>, i.e. it 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>&lt;E&gt;</code> is a reference type like {@link String}, * {@link Integer}, {@link Long}, {@link Timestamp}, etc., then converting from * <code>&lt;R&gt;</code> to <code>&lt;E&gt;</code> is mere convenience for * calling {@link Record#getValue(int, Class)} with * <code>fieldIndex = 0</code></li> * <li>If <code>&lt;E&gt;</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> * <p> * <h5>If <code>&lt;E&gt;</code> is a {@link TableRecord} type (e.g. from a * generated record), then its meta data are used:</h5> * <p> * Generated {@link TableRecord} types reference their corresponding generated * {@link Table} types, which provide {@link TableField} meta data through * {@link Table#fields()}. All target {@link Record#fields()} are looked up in * the source table via {@link Table#indexOf(Field)} and their values are * mapped. Excess source values and missing target values are ignored. * <p> * <h5>If a default constructor is available and any JPA {@link Column} * annotations are found on the provided <code>&lt;E&gt;</code>, only those are * used:</h5> * <p> * <ul> * <li>If <code>&lt;E&gt;</code> contains single-argument instance methods of * any visibility annotated with <code>Column</code>, those methods are * invoked</li> * <li>If <code>&lt;E&gt;</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>&lt;E&gt;</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 Kotlin is available and the argument class has Kotlin reflection meta * data available, and {@link Settings#isMapConstructorParameterNamesInKotlin()} * is turned on, parameter names are reflected and used.</h5> * <p> * <ul> * <li>The Kotlin compiler adds meta data available for reflection using Kotlin * reflection APIs to derive parameter names.</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()}). This choice is * non-deterministic as neither the JVM nor the JDK guarantee any order of * methods or constructors.</li> * <li>When {@link Settings#isMapConstructorParameterNames()} is turned on, and * parameter names are available through reflection on * {@link Executable#getParameters()}, then values are mapped by name, otherwise * by index. (see #4627)</li> * <li>When invoking the "matching" constructor, values are converted onto * constructor argument types</li> * </ul> * <p> * <h5>If no default constructor is available, no "matching" constructor is * available, but {@link Settings#isMapConstructorParameterNames()} is turned * on, and parameter names are available through reflection on * {@link Executable#getParameters()}, the first constructor is used</h5> * <p> * <ul> * <li>The first constructor is chosen (as reported by * {@link Class#getDeclaredConstructors()}). This choice is non-deterministic as * neither the JVM nor the JDK guarantee any order of methods or * constructors.</li> * <li>When invoking that 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>&lt;E&gt;</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.
/** * The record type. */
private final Field<?>[] fields; private final RecordType<R> rowType;
The target type.
/** * The target type. */
private final Class<? extends E> type;
The configuration in whose context this RecordMapper operates.

This configuration can be used for caching reflection information.

/** * 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 type.
/** * A delegate mapper created from type information in <code>type</code>. */
private RecordMapper<R, E> delegate;
A set of field name prefixes that may defined the behaviour of nested record mappers.
/** * A set of field name prefixes that may defined the behaviour of nested * record mappers. */
private transient Set<String> prefixes;
Create a new DefaultRecordMapper.

This constructor uses a new DefaultConfiguration internally to cache various reflection methods. For better performance, use DefaultRecordMapper(RecordType, Class, Configuration) instead.

/** * Create a new <code>DefaultRecordMapper</code>. * <p> * This constructor uses a new {@link DefaultConfiguration} internally to * cache various reflection methods. For better performance, use * {@link #DefaultRecordMapper(RecordType, Class, Configuration)} instead. */
public DefaultRecordMapper(RecordType<R> rowType, Class<? extends E> type) { this(rowType, type, null, null); }
Create a new DefaultRecordMapper.
/** * Create a new <code>DefaultRecordMapper</code>. */
public 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, Configuration configuration) { this.rowType = rowType; this.fields = rowType.fields(); this.type = type; this.configuration = configuration != null ? configuration : new DefaultConfiguration(); 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)) { RecordMapper<R, Object[]> local = configuration.recordMapperProvider().provide(rowType, Object[].class); delegate = r -> (E) Stream.of(local.map(r)); return; } // [#10071] Single-field Record1 types can be mapped if there is a ConverterProvider allowing for this mapping if (fields.length == 1 && Tools.converter(configuration, fields[0].getType(), type) != null) { 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 { MutablePOJOMapper m = new MutablePOJOMapper(new ConstructorCall<>(accessible(type.getDeclaredConstructor())), instance); // [#10194] Check if the POJO is really mutable. There might as well // be a no-args constructor for other reasons, e.g. when // using an immutable Kotlin data class with defaulted parameters // If the no-args constructor is the only one, take it none-theless if (m.isMutable() || type.getDeclaredConstructors().length <= 1) { delegate = m; 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(); // [#6868] Prefer public constructors Arrays.sort(constructors, new Comparator<Constructor<E>>() { @Override public int compare(Constructor<E> c1, Constructor<E> c2) { return (c2.getModifiers() & Modifier.PUBLIC) - (c1.getModifiers() & Modifier.PUBLIC); } }); // [#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 ImmutablePOJOMapperWithParameterNames(constructor, Arrays.asList(properties.value()), true); return; } } // [#7324] Map immutable Kotlin classes by parameter names if kotlin-reflect is on the classpath if (Tools.isKotlinAvailable() && !FALSE.equals(configuration.settings().isMapConstructorParameterNamesInKotlin())) { try { Reflect jvmClassMappingKt = Tools.ktJvmClassMapping(); Reflect kClasses = Tools.ktKClasses(); Reflect kTypeParameter = Tools.ktKTypeParameter(); Object klass = jvmClassMappingKt.call("getKotlinClass", type).get(); Reflect primaryConstructor = kClasses.call("getPrimaryConstructor", klass); // It is a Kotlin class if (primaryConstructor.get() != null) { List<?> parameters = primaryConstructor.call("getParameters").get(); Class<?> klassType = Tools.ktKClass().type(); Method getJavaClass = jvmClassMappingKt.type().getMethod("getJavaClass", klassType); List<String> parameterNames = new ArrayList<>(parameters.size()); Class<?>[] parameterTypes = new Class[parameters.size()]; for (int i = 0; i < parameterTypes.length; i++) { Reflect parameter = Reflect.on(parameters.get(i)); Object typeClassifier = parameter.call("getType").call("getClassifier").get(); String name = parameter.call("getName").<String>get(); // [#8578] If the constructor parameter is a KTypeParameter, we need an additional step to // extract the first upper bounds' classifier, which (hopefully) is a KClass parameterTypes[i] = (Class<?>) getJavaClass.invoke( jvmClassMappingKt.get(), (kTypeParameter.type().isInstance(typeClassifier) ? Reflect.on(typeClassifier).call("getUpperBounds").call("get", 0).call("getClassifier").get() : typeClassifier) ); // [#8004] Clean up kotlin field name for boolean types String typeName = parameterTypes[i].getName(); if (name.startsWith("is") && (boolean.class.getName().equalsIgnoreCase(typeName) || Boolean.class.getName().equals(typeName))) name = getPropertyName(name); parameterNames.add(name); } Constructor<E> javaConstructor = (Constructor<E>) accessible(this.type.getDeclaredConstructor(parameterTypes)); delegate = new ImmutablePOJOMapperWithParameterNames(javaConstructor, parameterNames, true); return; } } catch (ReflectException ignore) {} catch (NoSuchMethodException ignore) {} catch (IllegalAccessException ignore) {} catch (InvocationTargetException ignore) {} } boolean mapConstructorParameterNames = TRUE.equals(configuration.settings().isMapConstructorParameterNames()); // [#1837] Without ConstructorProperties, match constructors by matching // argument length // [#6598] Try prefixes first (for nested POJOs), and then field.length // (for a flat POJO) for (boolean supportsNesting : new boolean[] { true, false }) { for (Constructor<E> constructor : constructors) { Class<?>[] parameterTypes = constructor.getParameterTypes(); // Match the first constructor by parameter length if (parameterTypes.length == (supportsNesting ? prefixes().size() : fields.length)) { // [#4627] use parameter names from byte code if available if (mapConstructorParameterNames) { Parameter[] parameters = constructor.getParameters(); if (parameters != null && parameters.length > 0) delegate = new ImmutablePOJOMapperWithParameterNames(constructor, collectParameterNames(parameters), supportsNesting); } if (delegate == null) delegate = new ImmutablePOJOMapper(constructor, parameterTypes, supportsNesting); return; } } } // [#4627] if there is no exact match in terms of the number of parameters, // but using parameter annotations is allowed and those are in fact present, // use the first available constructor (thus the choice is undeterministic) if (mapConstructorParameterNames) { Constructor<E> constructor = constructors[0]; Parameter[] parameters = constructor.getParameters(); if (parameters != null && parameters.length > 0) { delegate = new ImmutablePOJOMapperWithParameterNames(constructor, collectParameterNames(parameters), false); return; } } throw new MappingException("No matching constructor found on type " + type + " for row type " + rowType); } private List<String> collectParameterNames(Parameter[] parameters) { return Arrays.stream(parameters).map(Parameter::getName).collect(Collectors.toList()); } @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.

The supplied type is usually Object[], but in some cases, it may make sense to supply String[], Integer[] etc.

/** * 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.

This is done for types that are not instanciable

/** * 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> { private Constructor<Lookup> constructor; private final MutablePOJOMapper pojomapper; ProxyMapper() { this.pojomapper = new MutablePOJOMapper(new Callable<E>() { @Override public E call() throws Exception { return proxy(); } }, null); } @Override public final E map(R record) { return pojomapper.map(record); } private E proxy() { final Object[] result = new Object[1]; final Map<String, Object> map = new HashMap<>(); 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.
/** * 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); } } } private static final class ConstructorCall<E> implements Callable<E> { private final Constructor<? extends E> constructor; ConstructorCall(Constructor<? extends E> constructor) { this.constructor = constructor; } @Override public E call() throws Exception { return constructor.newInstance(); } }
Convert a record into a mutable POJO type

jOOQ's understanding of a mutable POJO is a Java type that has a default constructor

/** * 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 Callable<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, NestedMappingInfo> nestedMappingInfos; private final E instance; MutablePOJOMapper(Callable<E> constructor, E instance) { this.constructor = constructor; this.useAnnotations = hasColumnAnnotations(configuration, type); this.members = new List[fields.length]; this.methods = new List[fields.length]; this.instance = instance; this.nestedMappingInfos = new HashMap<>(); Map<String, List<Field<?>>> nestedMappedFields = null; 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, true); methods[i] = getAnnotatedSetters(configuration, type, name, true); } // No annotations are present else { int dot = name.indexOf('.'); // A nested mapping is applied if (dot > -1) { String prefix = name.substring(0, dot); if (nestedMappedFields == null) nestedMappedFields = new HashMap<>(); List<Field<?>> f = nestedMappedFields.get(prefix); if (f == null) nestedMappedFields.put(prefix, f = new ArrayList<>()); NestedMappingInfo nestedMappingInfo = nestedMappingInfos.get(prefix); if (nestedMappingInfo == null) nestedMappingInfos.put(prefix, nestedMappingInfo = new NestedMappingInfo()); f.add(field(name(name.substring(prefix.length() + 1)), field.getDataType())); nestedMappingInfo.indexLookup.add(i); members[i] = Collections.emptyList(); methods[i] = Collections.emptyList(); } // A top-level mapping is applied else { members[i] = getMatchingMembers(configuration, type, name, true); methods[i] = getMatchingSetters(configuration, type, name, true); } } } if (nestedMappedFields != null) { for (Entry<String, List<Field<?>>> entry : nestedMappedFields.entrySet()) { String prefix = entry.getKey(); NestedMappingInfo nestedMappingInfo = nestedMappingInfos.get(prefix); nestedMappingInfo.row = Tools.row0(entry.getValue()); nestedMappingInfo.recordDelegate = newRecord(true, recordType(nestedMappingInfo.row.size()), nestedMappingInfo.row, configuration); for (java.lang.reflect.Field member : getMatchingMembers(configuration, type, prefix, true)) nestedMappingInfo.mappers.add(configuration .recordMapperProvider() .provide((RecordType<AbstractRecord>) nestedMappingInfo.row.fields, member.getType())); for (Method method : getMatchingSetters(configuration, type, prefix, true)) nestedMappingInfo.mappers.add(configuration .recordMapperProvider() .provide((RecordType<AbstractRecord>) nestedMappingInfo.row.fields, method.getParameterTypes()[0])); } } } final boolean isMutable() { for (List<Method> m : methods) if (!m.isEmpty()) return true; for (List<java.lang.reflect.Field> m1 : members) for (java.lang.reflect.Field m2 : m1) if ((m2.getModifiers() & Modifier.FINAL) == 0) return true; return false; } @Override public final E map(final R record) { try { final E result = instance != null ? instance : constructor.call(); 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] [#10910] Try mapping nested collection types Object list = tryConvertToList(value, mType, method.getGenericParameterTypes()[0]); if (list != null) method.invoke(result, list); else method.invoke(result, record.get(i, mType)); } } for (final Entry<String, NestedMappingInfo> entry : nestedMappingInfos.entrySet()) { final String prefix = entry.getKey(); for (final RecordMapper<AbstractRecord, Object> mapper : entry.getValue().mappers) { entry.getValue().recordDelegate.operate(new RecordOperation<AbstractRecord, Exception>() { @Override public AbstractRecord operate(AbstractRecord rec) throws Exception { List<Integer> indexes = entry.getValue().indexLookup; for (int index = 0; index < indexes.size(); index++) rec.set(index, record.get(indexes.get(index))); Object value = mapper.map(rec); for (java.lang.reflect.Field member : getMatchingMembers(configuration, type, prefix, true)) { // [#935] Avoid setting final fields if ((member.getModifiers() & Modifier.FINAL) == 0) map(value, result, member); } for (Method method : getMatchingSetters(configuration, type, prefix, true)) method.invoke(result, value); return rec; } }); } } return result; } catch (Exception e) { throw new MappingException("An error ocurred when mapping record to " + type, e); } } 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] [#10910] Try mapping nested collection types Object list = tryConvertToList(value, mType, member.getGenericType()); if (list != null) member.set(result, list); else map(value, result, member); } } private final List<?> tryConvertToList(Object value, Class<?> mType, Type genericType) { if (value instanceof Collection && (mType == List.class || mType == ArrayList.class) && genericType instanceof ParameterizedType) { Class<?> componentType = (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0]; return Convert.convert((Collection<?>) value, componentType); } else return null; } 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).
/** * Convert a record into an "immutable" POJO (final fields, "matching" * constructor). */
private class ImmutablePOJOMapper implements RecordMapper<R, E> { final Constructor<E> constructor; final Class<?>[] parameterTypes; private final boolean nested; private final int[] nonNestedIndexLookup; private final NestedMappingInfo[] nestedMappingInfo; ImmutablePOJOMapper(Constructor<E> constructor, Class<?>[] parameterTypes, boolean supportsNesting) { int size = prefixes().size(); this.constructor = accessible(constructor); this.parameterTypes = parameterTypes; this.nestedMappingInfo = new NestedMappingInfo[size]; this.nonNestedIndexLookup = new int[size]; int i = -1; boolean hasNestedFields = false; List<Field<?>>[] nestedMappedFields = new List[size]; if (supportsNesting) { prefixLoop: for (String prefix : prefixes()) { ++i; for (int j = 0; j < fields.length; j++) { if (fields[j].getName().equals(prefix)) { nonNestedIndexLookup[i] = j; continue prefixLoop; } else if (fields[j].getName().startsWith(prefix + ".")) { hasNestedFields = true; if (nestedMappedFields[i] == null) nestedMappedFields[i] = new ArrayList<>(); if (nestedMappingInfo[i] == null) nestedMappingInfo[i] = new NestedMappingInfo(); nestedMappedFields[i].add(field( name(fields[j].getName().substring(prefix.length() + 1)), fields[j].getDataType() )); nestedMappingInfo[i].indexLookup.add(j); } } if (nestedMappedFields[i] != null) { nestedMappingInfo[i].row = row0(nestedMappedFields[i].toArray(EMPTY_FIELD)); nestedMappingInfo[i].recordDelegate = newRecord(true, recordType(nestedMappingInfo[i].row.size()), nestedMappingInfo[i].row, configuration); nestedMappingInfo[i].mappers.add(configuration .recordMapperProvider() .provide((RecordType<AbstractRecord>) nestedMappingInfo[i].row.fields, parameterTypes[i])); } } } this.nested = hasNestedFields; } @Override public final E map(R record) { try { return constructor.newInstance(nested ? mapNested(record) : mapNonnested(record)); } catch (Exception e) { throw new MappingException("An error ocurred when mapping record to " + type, e); } } private final Object[] mapNonnested(R record) { Object[] converted = new Object[parameterTypes.length]; // [#10425] Initialise array to constructor parameter type init values for (int i = 0; i < converted.length; i++) converted[i] = Reflect.initValue(parameterTypes[i]); for (int i = 0; i < record.size(); i++) set(record, converted, i); return converted; } private final Object[] mapNested(final R record) { Object[] converted = new Object[prefixes().size()]; for (int i = 0; i < converted.length; i++) { if (nestedMappingInfo[i] == null) { converted[i] = record.get(nonNestedIndexLookup[i], parameterTypes[i]); } else { final List<Integer> indexLookup = nestedMappingInfo[i].indexLookup; converted[i] = nestedMappingInfo[i].mappers.get(0).map(nestedMappingInfo[i].recordDelegate.operate(new RecordOperation<AbstractRecord, RuntimeException>() { @Override public AbstractRecord operate(AbstractRecord rec) { for (int j = 0; j < indexLookup.size(); j++) rec.set(j, record.get(indexLookup.get(j))); return rec; } })); } } return converted; } void set(Record from, Object[] to, int index) { to[index] = from.get(index, parameterTypes[index]); } }
Create an immutable POJO given a constructor and its associated JavaBeans ConstructorProperties
/** * Create an immutable POJO given a constructor and its associated JavaBeans * {@link ConstructorProperties} */
private class ImmutablePOJOMapperWithParameterNames extends ImmutablePOJOMapper { 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; ImmutablePOJOMapperWithParameterNames(Constructor<E> constructor, List<String> propertyNames, boolean supportsNesting) { super(constructor, constructor.getParameterTypes(), supportsNesting); this.propertyNames = propertyNames; this.useAnnotations = hasColumnAnnotations(configuration, type); 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, false); methods[i] = getAnnotatedGetter(configuration, type, name, true); } // No annotations are present else { members[i] = getMatchingMembers(configuration, type, name, false); methods[i] = getMatchingGetter(configuration, type, name, true); } // [#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 void set(Record from, Object[] to, int i) { if (propertyIndexes[i] != null) { to[propertyIndexes[i]] = from.get(i, parameterTypes[propertyIndexes[i]]); } else { for (java.lang.reflect.Field member : members[i]) { int index = propertyNames.indexOf(member.getName()); if (index >= 0) to[index] = from.get(i, member.getType()); } if (methods[i] != null) { String name = getPropertyName(methods[i].getName()); int index = propertyNames.indexOf(name); if (index >= 0) to[index] = from.get(i, methods[i].getReturnType()); } } } } 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) if (Tools.attachRecords(record.configuration())) ((Attachable) attachable).attach(record.configuration()); return attachable; } private final Set<String> prefixes() { if (prefixes == null) { prefixes = new LinkedHashSet<>(); for (Field<?> field : fields) { String name = field.getName(); int dot = name.indexOf('.'); prefixes.add(dot > -1 ? name.substring(0, dot) : name); } } return prefixes; } static class NestedMappingInfo { final List<RecordMapper<AbstractRecord, Object>> mappers; AbstractRow row; final List<Integer> indexLookup; RecordDelegate<? extends AbstractRecord> recordDelegate; NestedMappingInfo() { mappers = new ArrayList<>(); indexLookup = new ArrayList<>(); } } }