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: 0.10.0
Show newest version
/**
 * Copyright (c) 2009-2014, Data Geekery GmbH (http://www.datageekery.com)
 * All rights reserved.
 *
 * This work is dual-licensed
 * - under the Apache Software License 2.0 (the "ASL")
 * - under the jOOQ License and Maintenance Agreement (the "jOOQ License")
 * =============================================================================
 * You may choose which license applies to you:
 *
 * - If you're using this work with Open Source databases, you may choose
 *   either ASL or jOOQ License.
 * - If you're using this work with at least one commercial database, you must
 *   choose jOOQ License
 *
 * For more information, please visit http://www.jooq.org/licenses
 *
 * Apache Software License 2.0:
 * -----------------------------------------------------------------------------
 * 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.
 *
 * jOOQ License and Maintenance Agreement:
 * -----------------------------------------------------------------------------
 * Data Geekery grants the Customer the non-exclusive, timely limited and
 * non-transferable license to install and use the Software under the terms of
 * the jOOQ License and Maintenance Agreement.
 *
 * This library is distributed with a LIMITED WARRANTY. See the jOOQ License
 * and Maintenance Agreement for more details: http://www.jooq.org/licensing
 */
package org.jooq.impl;

import static org.jooq.impl.Utils.getAnnotatedGetter;
import static org.jooq.impl.Utils.getAnnotatedMembers;
import static org.jooq.impl.Utils.getAnnotatedSetters;
import static org.jooq.impl.Utils.getMatchingGetter;
import static org.jooq.impl.Utils.getMatchingMembers;
import static org.jooq.impl.Utils.getMatchingSetters;
import static org.jooq.impl.Utils.getPropertyName;
import static org.jooq.impl.Utils.hasColumnAnnotations;
import static org.jooq.tools.reflect.Reflect.accessible;

import java.beans.ConstructorProperties;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

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.reflect.Reflect;

/**
 * This is the default implementation for RecordMapper types.
 * 

* 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 <E> is a field "value type" and <R> * has exactly one column:
*

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

*

    *
  • If <E> is a reference type like {@link String}, * {@link Integer}, {@link Long}, {@link Timestamp}, etc., then converting from * <R> to <E> is mere convenience for calling * {@link Record#getValue(int, Class)} 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 a default constructor is available and any JPA {@link Column} * annotations are found on the provided <E>, only those are * used:
*

*

    *
  • If <E> contains public single-argument instance methods * annotated with Column, those methods are invoked
  • *
  • If <E> contains public no-argument instance methods * starting with getXXX or isXXX, annotated with * Column, then matching public setXXX() instance * methods are invoked
  • *
  • If <E> contains public instance member fields 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 * javax.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: *

    *
  • Public single-argument instance method MY_field(...)
  • *
  • Public single-argument instance method myField(...)
  • *
  • Public single-argument instance method setMY_field(...)
  • *
  • Public single-argument instance method setMyField(...)
  • *
  • Public non-final instance member field MY_field
  • *
  • Public 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 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 * (as reported by {@link Class#getDeclaredConstructors()}
  • *
  • When invoking the annotated constructor, values are converted onto * constructor argument types
  • *
*

*

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()}
  • *
  • When invoking the "matching" 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 {@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)}. *

*

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; /** * 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; /** * An optional target instance to use instead of creating new instances. */ private transient E instance; /** * A delegate mapper created from type information in type. */ private RecordMapper delegate; public DefaultRecordMapper(RecordType rowType, Class type) { this(rowType, type, null, null); } DefaultRecordMapper(RecordType rowType, Class type, Configuration configuration) { this(rowType, type, null, configuration); } DefaultRecordMapper(RecordType rowType, Class type, E instance) { this(rowType, type, instance, null); } DefaultRecordMapper(RecordType rowType, Class type, E instance, Configuration configuration) { this.fields = rowType.fields(); this.type = type; this.instance = instance; this.configuration = configuration; init(); } private final void init() { // Arrays can be mapped easily if (type.isArray()) { delegate = new ArrayMapper(); return; } // [#3212] "Value types" can be mapped from single-field Record1 types for convenience if (type.isPrimitive() || DefaultDataType.types().contains(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) new RecordToRecordMapper(); return; } // [#1340] Allow for using non-public default constructors try { delegate = new MutablePOJOMapper(type.getDeclaredConstructor()); 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[] constructors = (Constructor[]) type.getDeclaredConstructors(); // [#1837] If any java.beans.ConstructorProperties annotations are // present use those rather than matching constructors by the number of // arguments for (Constructor 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 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. *

* The supplied type is usually Object[], but in some cases, it * may make sense to supply String[], Integer[] * etc. */ private class ArrayMapper implements RecordMapper { @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.getValue(i), componentType); } return (E) result; } } private class ValueTypeMapper implements RecordMapper { @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.getValue(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 implements RecordMapper { private final MutablePOJOMapper localDelegate; ProxyMapper() { this.localDelegate = new MutablePOJOMapper(null); } @Override public final E map(R record) { E previous = instance; try { instance = Reflect.on(HashMap.class).create().as(type); return localDelegate.map(record); } finally { instance = previous; } } } /** * Convert a record into another record type. */ private class RecordToRecordMapper implements RecordMapper { @Override public final AbstractRecord map(R record) { try { if (record instanceof AbstractRecord) { return ((AbstractRecord) record).intoRecord((Class) 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); } } } /** * 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 implements RecordMapper { private final Constructor constructor; private final boolean useAnnotations; private final List[] members; private final List[] methods; MutablePOJOMapper(Constructor constructor) { this.constructor = accessible(constructor); this.useAnnotations = hasColumnAnnotations(configuration, type); this.members = new List[fields.length]; this.methods = new List[fields.length]; for (int i = 0; i < fields.length; i++) { Field field = fields[i]; // Annotations are available and present if (useAnnotations) { members[i] = getAnnotatedMembers(configuration, type, field.getName()); methods[i] = getAnnotatedSetters(configuration, type, field.getName()); } // No annotations are present else { members[i] = getMatchingMembers(configuration, type, field.getName()); methods[i] = getMatchingSetters(configuration, type, field.getName()); } } } @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]) { method.invoke(result, record.getValue(i, method.getParameterTypes()[0])); } } 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) { member.setByte(result, record.getValue(index, byte.class)); } else if (mType == short.class) { member.setShort(result, record.getValue(index, short.class)); } else if (mType == int.class) { member.setInt(result, record.getValue(index, int.class)); } else if (mType == long.class) { member.setLong(result, record.getValue(index, long.class)); } else if (mType == float.class) { member.setFloat(result, record.getValue(index, float.class)); } else if (mType == double.class) { member.setDouble(result, record.getValue(index, double.class)); } else if (mType == boolean.class) { member.setBoolean(result, record.getValue(index, boolean.class)); } else if (mType == char.class) { member.setChar(result, record.getValue(index, char.class)); } } else { member.set(result, record.getValue(index, mType)); } } } /** * Convert a record into an "immutable" POJO (final fields, "matching" * constructor). */ private class ImmutablePOJOMapper implements RecordMapper { private final Constructor constructor; private final Class[] parameterTypes; public ImmutablePOJOMapper(Constructor 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 { private final Constructor constructor; private final Class[] parameterTypes; private final Object[] parameterValues; private final List propertyNames; private final boolean useAnnotations; private final List[] members; private final java.lang.reflect.Method[] methods; ImmutablePOJOMapperWithConstructorProperties(Constructor 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]; for (int i = 0; i < fields.length; i++) { Field field = fields[i]; // Annotations are available and present if (useAnnotations) { members[i] = getAnnotatedMembers(configuration, type, field.getName()); methods[i] = getAnnotatedGetter(configuration, type, field.getName()); } // No annotations are present else { members[i] = getMatchingMembers(configuration, type, field.getName()); methods[i] = getMatchingGetter(configuration, type, field.getName()); } } } @Override public final E map(R record) { try { for (int i = 0; i < fields.length; i++) { for (java.lang.reflect.Field member : members[i]) { int index = propertyNames.indexOf(member.getName()); if (index >= 0) { parameterValues[index] = record.getValue(i); } } if (methods[i] != null) { String name = getPropertyName(methods[i].getName()); int index = propertyNames.indexOf(name); if (index >= 0) { parameterValues[index] = record.getValue(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 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 (Utils.attachRecords(r.configuration())) { a.attach(r.configuration()); } } return attachable; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy