All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jooq.impl.DefaultRecordMapper Maven / Gradle / Ivy

There is a newer version: 3.19.15
Show newest version
/*
 * 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
 *
 *  https://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
 * Apache-2.0 and offer limited warranties, support, maintenance, and commercial
 * database integrations.
 *
 * For more information, please visit: https://www.jooq.org/legal/licensing
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package org.jooq.impl;

import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static java.util.Collections.emptyList;
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.configuration;
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.map;
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;
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.RecordComponent;
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.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jakarta.persistence.Column;

import org.jooq.Attachable;
import org.jooq.Configuration;
import org.jooq.Converter;
import org.jooq.ConverterProvider;
import org.jooq.Field;
import org.jooq.JSON;
import org.jooq.JSONB;
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.XML;
import org.jooq.conf.Settings;
import org.jooq.exception.MappingException;
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 {@link Record#into(Class)}, {@link 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 {@link 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 {@link MappingException} is thrown, wrapping * conversion exceptions. *

*

If the supplied type is an interface or an abstract class
*

* 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)}. *

*

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

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

*

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

* The configured {@link ConverterProvider} is used to look up a * {@link Converter} between T1 and E. By default, the * {@link DefaultConverterProvider} is used, which can (among other things): *

    *
  • Map between built-in types
  • *
  • Map between {@link Record} types and custom types by delegating to the * {@link Record}'s attached {@link RecordMapperProvider}
  • *
  • Map between {@link JSON} or {@link JSONB} and custom types by delegating * to Jackson or Gson (if found on the classpath)
  • *
  • Map between {@link XML} and custom types by delegating to JAXB (if found * on the classpath)
  • *
* If such a {@link Converter} is found, that one is used to map to * E. *

*

If a default constructor is available and any JPA {@link 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
  • *
  • {@link Column#name()} must match {@link 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 * jakarta.persistence API on the classpath, jOOQ will map * Record values by naming convention:
*

* If {@link 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 {@link 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 {@link 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 {@link 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 {@link Class#getDeclaredConstructors()}). This choice is * non-deterministic as neither the JVM nor the JDK guarantee any order of * methods or constructors.
  • *
  • 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)
  • *
  • When invoking the "matching" constructor, values are converted onto * constructor argument types
  • *
*

*

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
*

*

    *
  • 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.
  • *
  • When invoking that constructor, values are converted onto constructor * argument types
  • *
*

*

Other restrictions
*

*

    *
  • <E> must provide a default or a "matching" * constructor. Non-public default constructors are made accessible using * {@link Constructor#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 {@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 implements RecordMapper { /** * The record type. */ private final Field[] fields; private final RecordType rowType; /** * The target type. */ private final Class type; /** * The configuration in whose context this {@link RecordMapper} operates. *

* This configuration can be used for caching reflection information. */ private final Configuration configuration; private final String namePathSeparator; /** * A delegate mapper created from type information in type. */ private RecordMapper delegate; /** * A set of field name prefixes that may defined the behaviour of nested * record mappers. */ private transient Map prefixes; /** * Create a new DefaultRecordMapper. *

* 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 rowType, Class type) { this(rowType, type, null, null); } /** * Create a new DefaultRecordMapper. */ public DefaultRecordMapper(RecordType rowType, Class type, Configuration configuration) { this(rowType, type, null, configuration); } DefaultRecordMapper(RecordType rowType, Class type, E instance, Configuration configuration) { this.rowType = rowType; this.fields = rowType.fields(); this.type = type; this.configuration = configuration(configuration); this.namePathSeparator = this.configuration.settings().getNamePathSeparator(); init(instance); } private final void init(E instance) { Boolean debugVTFL = null; Boolean debugVTCP = null; Boolean debugMutable = null; Boolean debugMutableConstructors = null; Boolean debugCPSettings = null; Boolean debugRC = null; Boolean debugRCSettings = null; Boolean debugKClass = null; Boolean debugKSettings = null; Boolean debugMatchDegreeFlat = null; Boolean debugMatchDegreeNested = null; Boolean debugTopLevelClass = null; Boolean debugStaticNestedClass = null; // Arrays can be mapped easily if (type.isArray()) { delegate = new ArrayMapper(instance); return; } if (instance == null) { if (Stream.class.isAssignableFrom(type)) { delegate = r -> (E) Stream.of(((FieldsImpl) rowType).mapper(configuration, Object[].class).map(r)); return; } // [#1470] Return a proxy if the supplied type is an interface // [#10071] [#11148] Primitive types are abstract! They're mapped by a ConverterProvider only later if (Modifier.isAbstract(type.getModifiers()) && !type.isPrimitive()) { delegate = new ProxyMapper(); return; } } // [#2989] [#2836] Records are mapped if (AbstractRecord.class.isAssignableFrom(type)) { delegate = (RecordMapper) new RecordToRecordMapper<>((AbstractRecord) instance); return; } // [#10071] Single-field Record1 types can be mapped if there is a ConverterProvider allowing for this mapping if ((debugVTFL = fields.length == 1) && instance == null && (debugVTCP = Tools.converter(configuration, instance, (Class) fields[0].getType(), type) != null)) { delegate = new ValueTypeMapper(); return; } // [#1340] Allow for using non-public default constructors try { MutablePOJOMapper m = instance != null ? new MutablePOJOMapper(null, instance) : new MutablePOJOMapper(new ConstructorCall<>(accessible(type.getDeclaredConstructor())), null); // [#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 ((debugMutable = m.isMutable()) || (debugMutableConstructors = type.getDeclaredConstructors().length <= 1)) { delegate = m; return; } } catch (NoSuchMethodException ignore) { debugMutable = false; } // [#1336] If no default constructor is present, check if there is a // "matching" constructor with the same number of fields as this record Constructor[] constructors = (Constructor[]) type.getDeclaredConstructors(); // [#6868] Prefer public constructors Arrays.sort(constructors, (c1, c2) -> (c2.getModifiers() & Modifier.PUBLIC) - (c1.getModifiers() & Modifier.PUBLIC)); // [#1837] [#10349] [#11123] If any java.beans.ConstructorProperties annotations are // present use those rather than matching constructors by the number of arguments if (debugCPSettings = !FALSE.equals(configuration.settings().isMapConstructorPropertiesParameterNames())) { for (Constructor constructor : constructors) { ConstructorProperties properties = constructor.getAnnotation(ConstructorProperties.class); if (properties != null) { delegate = new ImmutablePOJOMapper(constructor, constructor.getParameterTypes(), Arrays.asList(properties.value()), true); return; } } } // [#11778] Java 16 record types expose their component names if ((debugRCSettings = TRUE.equals(configuration.settings().isMapRecordComponentParameterNames())) && (debugRC = type.isRecord())) { RecordComponent[] rc = type.getRecordComponents(); List types = Tools.map(rc, RecordComponent::getType); for (Constructor constructor : constructors) { Class[] parameterTypes = constructor.getParameterTypes(); if (types.equals(Arrays.asList(parameterTypes))) { delegate = new ImmutablePOJOMapper( constructor, parameterTypes, Tools.map(rc, RecordComponent::getName), true ); return; } } } // [#7324] Map immutable Kotlin classes by parameter names if kotlin-reflect is on the classpath if ((debugKClass = Tools.isKotlinAvailable()) && (debugKSettings = !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 (debugKClass = primaryConstructor.get() != null) { List parameters = primaryConstructor.call("getParameters").get(); Class klassType = Tools.ktKClass().type(); Method getJavaClass = jvmClassMappingKt.type().getMethod("getJavaClass", klassType); List 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").get(); // [#14283] Unnest @JvmInline value classes try { while (Reflect.on(typeClassifier).call("isValue").get()) { typeClassifier = kClasses .call("getPrimaryConstructor", typeClassifier) .call("getParameters") // kotlin value classes are required to have exactly 1 parameter .call("get", 0) .call("getType") .call("getClassifier") .get(); } } // [#14283] KClass.isValue() was added in kotlin 1.5 only catch (ReflectException ignore) {} // [#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 javaConstructor = (Constructor) accessible(this.type.getDeclaredConstructor(parameterTypes)); delegate = new ImmutablePOJOMapper(javaConstructor, javaConstructor.getParameterTypes(), parameterNames, true); return; } } catch (ReflectException | InvocationTargetException | IllegalAccessException | NoSuchMethodException 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 constructor : constructors) { Class[] parameterTypes = constructor.getParameterTypes(); // Match the first constructor by parameter length if (parameterTypes.length == (supportsNesting ? prefixes().size() : fields.length)) { if (supportsNesting) debugMatchDegreeNested = true; else debugMatchDegreeFlat = true; // [#4627] use parameter names from byte code if available if (mapConstructorParameterNames) { Parameter[] parameters = constructor.getParameters(); if (parameters != null && parameters.length > 0) delegate = new ImmutablePOJOMapper(constructor, parameterTypes, collectParameterNames(parameters), supportsNesting); } if (delegate == null) delegate = new ImmutablePOJOMapper(constructor, parameterTypes, emptyList(), supportsNesting); return; } } if (supportsNesting) debugMatchDegreeNested = false; else debugMatchDegreeFlat = false; } // [#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 constructor = constructors[0]; Parameter[] parameters = constructor.getParameters(); if (parameters != null && parameters.length > 0) { delegate = new ImmutablePOJOMapper(constructor, constructor.getParameterTypes(), collectParameterNames(parameters), false); return; } } debugTopLevelClass = !type.isMemberClass(); debugStaticNestedClass = type.isMemberClass() && Modifier.isStatic(type.getModifiers()); throw new MappingException( """ No DefaultRecordMapper strategy applies to type $type for row type $rowType. Attempted strategies include (in this order): - Is type an array (false)? - Is type a Stream (false)? - Does row type have only 1 column ($debugVTFL) and did ConverterProvider provide a Converter for type ($debugVTCP)? - Is type abstract (false)? - Is type a org.jooq.Record (false)? - Is type a mutable POJO (a POJO with setters or non-final members: $debugMutable) and has a no-args constructor ($debugMutableConstructors)? - Does type have a @ConstructorProperties annotated constructor (false) and is Settings.mapConstructorPropertiesParameterNames enabled ($debugCPSettings)? - Is type a java.lang.Record ($debugRC) and is Settings.mapRecordComponentParameterNames enabled ($debugRCSettings)? - Is type a kotlin class ($debugKClass) and is Settings.mapConstructorParameterNamesInKotlin enabled ($debugKSettings)? - Is there a constructor that matches row type's degrees with nested fields ($debugMatchDegreeNested) or flat fields ($debugMatchDegreeFlat) - Is the type a top level class ($debugTopLevelClass) or static nested class ($debugStaticNestedClass)? - (Inner classes cannot be created via reflection) - Is Settings.mapConstructorParameterNames enabled ($debugMatchNames) """.replace("$type", type.toString()) .replace("$rowType", rowType.toString()) .replace("$debugVTFL", debug(debugVTFL)) .replace("$debugVTCP", debug(debugVTCP)) .replace("$debugCPSettings", debug(debugCPSettings)) .replace("$debugMutableConstructors", debug(debugMutableConstructors)) .replace("$debugMutable", debug(debugMutable)) .replace("$debugRCSettings", debug(debugRCSettings)) .replace("$debugRC", debug(debugRC)) .replace("$debugKClass", debug(debugKClass)) .replace("$debugKSettings", debug(debugKSettings)) .replace("$debugMatchDegreeNested", debug(debugMatchDegreeNested)) .replace("$debugMatchDegreeFlat", debug(debugMatchDegreeFlat)) .replace("$debugMatchNames", debug(mapConstructorParameterNames)) .replace("$debugTopLevelClass", debug(debugTopLevelClass)) .replace("$debugStaticNestedClass", debug(debugStaticNestedClass)) ); } private static final String debug(Boolean debug) { return debug == null ? "check skipped" : debug.toString(); } private List 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 occurred when mapping record to " + type, e); } } private abstract class AbstractDelegateMapper implements RecordMapper { @Override public String toString() { return getClass().getSimpleName() + " [ (" + rowType + ") -> " + type + "]"; } } /** * 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. */ private class ArrayMapper extends AbstractDelegateMapper { 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 extends AbstractDelegateMapper { @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 */ private class ProxyMapper extends AbstractDelegateMapper { private final MutablePOJOMapper pojomapper; ProxyMapper() { this.pojomapper = new MutablePOJOMapper(() -> Reflect.on(new HashMap<>()).as(type), null); } @Override public final E map(R record) { return pojomapper.map(record); } } /** * Convert a record into another record type. */ private class RecordToRecordMapper extends AbstractDelegateMapper { private final E instance; RecordToRecordMapper(E instance) { this.instance = instance; } @Override public final AbstractRecord map(R record) { try { if (record instanceof AbstractRecord a) { if (instance != null) return a.intoRecord(instance); else return a.intoRecord((Class) type); } throw new MappingException("Cannot map record " + record + " to type " + type); } catch (Exception e) { throw new MappingException("An error occurred when mapping record to " + type, e); } } } private static final record ConstructorCall(Constructor constructor) implements Callable { @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 */ private class MutablePOJOMapper extends AbstractDelegateMapper { private final Callable constructor; private final boolean useAnnotations; private final List[] members; private final List[] methods; private final Map nestedMappingInfos; private final E instance; MutablePOJOMapper(Callable 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>> 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 separator = name.indexOf(namePathSeparator); // A nested mapping is applied if (separator > -1) { String prefix = name.substring(0, separator); if (nestedMappedFields == null) nestedMappedFields = new HashMap<>(); nestedMappedFields .computeIfAbsent(prefix, p -> new ArrayList<>()) .add(field(name(name.substring(prefix.length() + 1)), field.getDataType())); nestedMappingInfos .computeIfAbsent(prefix, p -> new 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) { nestedMappedFields.forEach((prefix, list) -> { NestedMappingInfo nestedMappingInfo = nestedMappingInfos.get(prefix); nestedMappingInfo.row = Tools.row0(list); 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( nestedMappingInfo.row.fields.mapper(configuration, member.getType()) ); for (Method method : getMatchingSetters(configuration, type, prefix, true)) nestedMappingInfo.mappers.add( nestedMappingInfo.row.fields.mapper(configuration, method.getParameterTypes()[0]) ); }); } } final boolean isMutable() { for (List m : methods) if (!m.isEmpty()) return true; for (List m1 : members) for (java.lang.reflect.Field m2 : m1) if ((m2.getModifiers() & Modifier.FINAL) == 0) return true; return false; } @Override public final E map(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 entry : nestedMappingInfos.entrySet()) { final String prefix = entry.getKey(); for (final RecordMapper mapper : entry.getValue().mappers) { entry.getValue().recordDelegate.operate(rec -> { List 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 occurred 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] [#11213] 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). */ private class ImmutablePOJOMapper extends AbstractDelegateMapper { final Constructor constructor; final Class[] parameterTypes; private final boolean nested; private final NestedMappingInfo[] nestedMappingInfo; private final Integer[] propertyIndexes; // For named parameter mappings where the mapping is governed by JPA annotations private final List propertyNames; private final boolean useAnnotations; private final List[] members; private final java.lang.reflect.Method[] methods; ImmutablePOJOMapper(Constructor constructor, Class[] parameterTypes, List propertyNames, boolean supportsNesting) { int size = prefixes().size(); this.constructor = accessible(constructor); this.parameterTypes = parameterTypes; this.nestedMappingInfo = new NestedMappingInfo[size]; this.propertyIndexes = new Integer[fields.length]; this.propertyNames = propertyNames; this.useAnnotations = hasColumnAnnotations(configuration, type); this.members = new List[fields.length]; this.methods = new Method[fields.length]; if (propertyNames.isEmpty()) { if (!supportsNesting) { for (int i = 0; i < fields.length; i++) propertyIndexes[i] = i; } else { for (int i = 0; i < fields.length; i++) { Field field = fields[i]; String name = field.getName(); int separator = name.indexOf(namePathSeparator); propertyIndexes[i] = prefixes().get(separator > -1 ? name.substring(0, separator) : name); } } } else { fieldLoop: 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; continue fieldLoop; } } for (int j = 0; j < propertyNames.size(); j++) { if (name.startsWith(propertyNames.get(j) + namePathSeparator)) { propertyIndexes[i] = j; continue fieldLoop; } } } } boolean hasNestedFields = false; List>[] nestedMappedFields = new List[size]; if (supportsNesting) { prefixLoop: for (Entry entry : prefixes().entrySet()) { String prefix = entry.getKey(); int i = entry.getValue(); if (nestedMappingInfo[i] == null) nestedMappingInfo[i] = new NestedMappingInfo(); for (int j = 0; j < fields.length; j++) { if (fields[j].getName().equals(prefix)) { nestedMappingInfo[i].indexLookup.add(j); continue prefixLoop; } } for (int j = 0; j < fields.length; j++) { if (fields[j].getName().startsWith(prefix + namePathSeparator)) { hasNestedFields = true; if (nestedMappedFields[i] == null) nestedMappedFields[i] = new ArrayList<>(); 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( nestedMappingInfo[i].row.fields.mapper(configuration, parameterTypes[propertyIndexes[nestedMappingInfo[i].indexLookup.get(0)]]) ); } } } 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 occurred when mapping record to " + type, e); } } private final Object[] mapNonnested(R record) { // [#10425] Initialise array to constructor parameter type init values Object[] converted = Tools.map(parameterTypes, c -> Reflect.initValue(c), Object[]::new); for (int i = 0; i < record.size(); i++) set(record, i, converted, propertyIndexes[i]); return converted; } final void set(Record from, int fromIndex, Object[] to, Integer toIndex) { // TODO: This logic could be applicable to mapNested() as well? if (toIndex != null) { to[toIndex] = from.get(fromIndex, parameterTypes[toIndex]); } else { for (java.lang.reflect.Field member : members[fromIndex]) { int index = propertyNames.indexOf(member.getName()); if (index >= 0) to[index] = from.get(fromIndex, member.getType()); } if (methods[fromIndex] != null) { String name = getPropertyName(methods[fromIndex].getName()); int index = propertyNames.indexOf(name); if (index >= 0) to[index] = from.get(fromIndex, methods[fromIndex].getReturnType()); } } } private final Object[] mapNested(final R record) { Object[] converted = new Object[parameterTypes.length]; for (int i = 0; i < nestedMappingInfo.length; i++) { NestedMappingInfo info = nestedMappingInfo[i]; List indexLookup = info.indexLookup; Integer j = indexLookup.get(0); Integer k = propertyIndexes[j]; if (k != null) { if (info.row == null) converted[k] = record.get(j, parameterTypes[k]); else converted[k] = info.mappers.get(0).map(info.recordDelegate.operate(rec -> { for (int x = 0; x < indexLookup.size(); x++) rec.set(x, record.get(indexLookup.get(x))); return rec; })); } } return converted; } @Override public String toString() { return getClass().getSimpleName() + " [ (" + rowType + ") -> " + constructor + "]"; } } private static 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 a) if (Tools.attachRecords(record.configuration())) a.attach(record.configuration()); return attachable; } private final Map prefixes() { if (prefixes == null) { prefixes = new LinkedHashMap<>(); int[] i = { 0 }; for (Field field : fields) { String name = field.getName(); int separator = name.indexOf(namePathSeparator); prefixes.computeIfAbsent(separator > -1 ? name.substring(0, separator) : name, k -> i[0]++); } } return prefixes; } static class NestedMappingInfo { final List> mappers; AbstractRow row; final List indexLookup; RecordDelegate recordDelegate; NestedMappingInfo() { mappers = new ArrayList<>(); indexLookup = new ArrayList<>(); } @Override public String toString() { return "NestedMappingInfo " + indexLookup + "; (" + row + ")"; } } @Override public String toString() { return delegate.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy