From 22b37800ec57d895069baeead98413e39747ecd1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Jul 2017 12:07:20 +0200 Subject: [PATCH] DATACMNS-1126 - Add class-generating entity instantiation support for Kotlin. We now discover Kotlin constructors and apply parameter defaulting to allow construction of immutable value objects. Constructor discovery uses primary constructors by default and considers PersistenceConstructor annotations. KotlinClassGeneratingEntityInstantiator can instantiate Kotlin classes with default parameters by resolving the synthetic constructor. Null values translate to parameter defaults. class Person(val firstname: String = "Walter") { } class Address(val street: String, val city: String) { @PersistenceConstructor constructor(street: String = "Unknown", city: String = "Unknown", country: String) : this(street, city) } Original pull request: #233. --- pom.xml | 43 ++++ .../ClassGeneratingEntityInstantiator.java | 100 ++++++-- .../data/convert/EntityInstantiators.java | 13 +- ...tlinClassGeneratingEntityInstantiator.java | 238 ++++++++++++++++++ .../mapping/model/BasicPersistentEntity.java | 28 +-- .../model/PreferredConstructorDiscoverer.java | 130 ++-------- .../PreferredConstructorDiscoverers.java | 183 ++++++++++++++ .../data/repository/query/ReturnedType.java | 4 +- .../data/util/ReflectionUtils.java | 17 ++ ...GeneratingEntityInstantiatorUnitTests.java | 36 ++- ...ReflectionEntityInstantiatorUnitTests.java | 6 +- ...eferredConstructorDiscovererUnitTests.java | 47 ++-- ...ssGeneratingEntityInstantiatorUnitTests.kt | 98 ++++++++ ...ionEntityInstantiatorDataClassUnitTests.kt | 77 ++++++ ...PreferredConstructorDiscovererUnitTests.kt | 90 +++++++ 15 files changed, 894 insertions(+), 216 deletions(-) create mode 100644 src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java create mode 100644 src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverers.java create mode 100644 src/test/kotlin/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiatorUnitTests.kt create mode 100644 src/test/kotlin/org/springframework/data/convert/ReflectionEntityInstantiatorDataClassUnitTests.kt create mode 100644 src/test/kotlin/org/springframework/data/mapping/model/PreferredConstructorDiscovererUnitTests.kt diff --git a/pom.xml b/pom.xml index ea6303c6e..b9d6ccfd8 100644 --- a/pom.xml +++ b/pom.xml @@ -231,6 +231,49 @@ test + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin} + true + + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin} + true + + + + org.jetbrains.kotlin + kotlin-test + ${kotlin} + test + + + + com.nhaarman + mockito-kotlin + 1.5.0 + test + + + org.jetbrains.kotlin + kotlin-stdlib + + + org.jetbrains.kotlin + kotlin-reflect + + + org.mockito + mockito-core + + + + org.scala-lang diff --git a/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java index deed67e84..59073f2a6 100644 --- a/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java +++ b/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java @@ -21,10 +21,8 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.security.AccessController; import java.security.PrivilegedAction; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.springframework.asm.ClassWriter; @@ -57,6 +55,19 @@ import org.springframework.util.ClassUtils; */ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { + private static final int ARG_CACHE_SIZE = 100; + + private static final ThreadLocal OBJECT_POOL = ThreadLocal.withInitial(() -> { + + Object[][] cached = new Object[ARG_CACHE_SIZE][]; + + for (int i = 0; i < ARG_CACHE_SIZE; i++) { + cached[i] = new Object[i]; + } + + return cached; + }); + private final ObjectInstantiatorClassGenerator generator; private volatile Map, EntityInstantiator> entityInstantiators = new HashMap<>(32); @@ -120,12 +131,20 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { } try { - return new EntityInstantiatorAdapter(createObjectInstantiator(entity)); + return doCreateEntityInstantiator(entity); } catch (Throwable ex) { return ReflectionEntityInstantiator.INSTANCE; } } + /** + * @param entity + * @return + */ + protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity entity) { + return new EntityInstantiatorAdapter(createObjectInstantiator(entity, entity.getPersistenceConstructor())); + } + /** * @param entity * @return @@ -151,17 +170,46 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { } /** - * Creates a dynamically generated {@link ObjectInstantiator} for the given {@link PersistentEntity}. There will - * always be exactly one {@link ObjectInstantiator} instance per {@link PersistentEntity}. - *

+ * Allocates an object array for instance creation. This method uses the argument array cache if possible. + * + * @param argumentCount + * @return + * @since 2.0 + * @see #ARG_CACHE_SIZE + */ + static Object[] allocateArguments(int argumentCount) { + return argumentCount < ARG_CACHE_SIZE ? OBJECT_POOL.get()[argumentCount] : new Object[argumentCount]; + } + + /** + * Deallocates an object array used for instance creation. Parameters are cleared if the array was cached. + * + * @param argumentCount + * @return + * @since 2.0 + * @see #ARG_CACHE_SIZE + */ + static void deallocateArguments(Object[] params) { + + if (params.length != 0 && params.length < ARG_CACHE_SIZE) { + Arrays.fill(params, null); + } + } + + /** + * Creates a dynamically generated {@link ObjectInstantiator} for the given {@link PersistentEntity} and + * {@link PreferredConstructor}. There will always be exactly one {@link ObjectInstantiator} instance per + * {@link PersistentEntity}. * * @param entity + * @param constructor * @return */ - private ObjectInstantiator createObjectInstantiator(PersistentEntity entity) { + ObjectInstantiator createObjectInstantiator(PersistentEntity entity, + @Nullable PreferredConstructor constructor) { try { - return (ObjectInstantiator) this.generator.generateCustomInstantiatorClass(entity).newInstance(); + return (ObjectInstantiator) this.generator.generateCustomInstantiatorClass(entity, constructor).newInstance(); } catch (Exception e) { throw new RuntimeException(e); } @@ -172,11 +220,10 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { * * @author Thomas Darimont * @author Oliver Gierke + * @author Mark Paluch */ private static class EntityInstantiatorAdapter implements EntityInstantiator { - private static final Object[] EMPTY_ARRAY = new Object[0]; - private final ObjectInstantiator instantiator; /** @@ -203,6 +250,8 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { return (T) instantiator.newInstance(params); } catch (Exception e) { throw new MappingInstantiationException(entity, Arrays.asList(params), e); + } finally { + deallocateArguments(params); } } @@ -216,17 +265,18 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { private

, T> Object[] extractInvocationArguments( @Nullable PreferredConstructor constructor, ParameterValueProvider

provider) { - if (provider == null || constructor == null || !constructor.hasParameters()) { - return EMPTY_ARRAY; + if (constructor == null || !constructor.hasParameters()) { + return allocateArguments(0); } - List params = new ArrayList<>(constructor.getConstructor().getParameterCount()); + Object[] params = allocateArguments(constructor.getConstructor().getParameterCount()); + int index = 0; for (Parameter parameter : constructor.getParameters()) { - params.add(provider.getParameterValue(parameter)); + params[index++] = provider.getParameterValue(parameter); } - return params.toArray(); + return params; } } @@ -290,7 +340,7 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { private final ByteArrayClassLoader classLoader; - private ObjectInstantiatorClassGenerator() { + ObjectInstantiatorClassGenerator() { this.classLoader = AccessController.doPrivileged( (PrivilegedAction) () -> new ByteArrayClassLoader(ClassUtils.getDefaultClassLoader())); @@ -300,12 +350,14 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { * Generate a new class for the given {@link PersistentEntity}. * * @param entity + * @param constructor * @return */ - public Class generateCustomInstantiatorClass(PersistentEntity entity) { + public Class generateCustomInstantiatorClass(PersistentEntity entity, + @Nullable PreferredConstructor constructor) { String className = generateClassName(entity); - byte[] bytecode = generateBytecode(className, entity); + byte[] bytecode = generateBytecode(className, entity, constructor); return classLoader.loadClass(className, bytecode); } @@ -323,9 +375,11 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { * * @param internalClassName * @param entity + * @param constructor * @return */ - public byte[] generateBytecode(String internalClassName, PersistentEntity entity) { + public byte[] generateBytecode(String internalClassName, PersistentEntity entity, + @Nullable PreferredConstructor constructor) { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); @@ -334,7 +388,7 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { visitDefaultConstructor(cw); - visitCreateMethod(cw, entity); + visitCreateMethod(cw, entity, constructor); cw.visitEnd(); @@ -357,8 +411,10 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { * * @param cw * @param entity + * @param constructor */ - private void visitCreateMethod(ClassWriter cw, PersistentEntity entity) { + private void visitCreateMethod(ClassWriter cw, PersistentEntity entity, + @Nullable PreferredConstructor constructor) { String entityTypeResourcePath = Type.getInternalName(entity.getType()); @@ -368,8 +424,6 @@ public class ClassGeneratingEntityInstantiator implements EntityInstantiator { mv.visitTypeInsn(NEW, entityTypeResourcePath); mv.visitInsn(DUP); - PreferredConstructor constructor = entity.getPersistenceConstructor(); - if (constructor != null) { Constructor ctor = constructor.getConstructor(); diff --git a/src/main/java/org/springframework/data/convert/EntityInstantiators.java b/src/main/java/org/springframework/data/convert/EntityInstantiators.java index 737146eff..cf05f47a8 100644 --- a/src/main/java/org/springframework/data/convert/EntityInstantiators.java +++ b/src/main/java/org/springframework/data/convert/EntityInstantiators.java @@ -24,10 +24,11 @@ import org.springframework.util.Assert; /** * Simple value object allowing access to {@link EntityInstantiator} instances for a given type falling back to a * default one. - * + * * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl + * @author Mark Paluch */ public class EntityInstantiators { @@ -43,7 +44,7 @@ public class EntityInstantiators { /** * Creates a new {@link EntityInstantiators} using the given {@link EntityInstantiator} as fallback. - * + * * @param fallback must not be {@literal null}. */ public EntityInstantiators(EntityInstantiator fallback) { @@ -52,17 +53,17 @@ public class EntityInstantiators { /** * Creates a new {@link EntityInstantiators} using the default fallback instantiator and the given custom ones. - * + * * @param customInstantiators must not be {@literal null}. */ public EntityInstantiators(Map, EntityInstantiator> customInstantiators) { - this(new ClassGeneratingEntityInstantiator(), customInstantiators); + this(new ClassGeneratingKotlinEntityInstantiator(), customInstantiators); } /** * Creates a new {@link EntityInstantiator} using the given fallback {@link EntityInstantiator} and the given custom * ones. - * + * * @param defaultInstantiator must not be {@literal null}. * @param customInstantiators must not be {@literal null}. */ @@ -78,7 +79,7 @@ public class EntityInstantiators { /** * Returns the {@link EntityInstantiator} to be used to create the given {@link PersistentEntity}. - * + * * @param entity must not be {@literal null}. * @return will never be {@literal null}. */ diff --git a/src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java new file mode 100644 index 000000000..03b94e61e --- /dev/null +++ b/src/main/java/org/springframework/data/convert/KotlinClassGeneratingEntityInstantiator.java @@ -0,0 +1,238 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.convert; + +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.jvm.ReflectJvmMapping; + +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.stream.IntStream; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Kotlin-specific extension to {@link ClassGeneratingEntityInstantiator} that adapts Kotlin constructors with + * defaulting. + * + * @author Mark Paluch + * @since 2.0 + */ +public class ClassGeneratingKotlinEntityInstantiator extends ClassGeneratingEntityInstantiator { + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.ClassGeneratingEntityInstantiator#doCreateEntityInstantiator(org.springframework.data.mapping.PersistentEntity) + */ + @Override + protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity entity) { + + if (ReflectionUtils.isKotlinClass(entity.getType()) && entity.getPersistenceConstructor() != null) { + + PreferredConstructor defaultConstructor = new DefaultingKotlinConstructorResolver(entity) + .getDefaultConstructor(); + + if (defaultConstructor != null) { + return new DefaultingKotlinClassInstantiatorAdapter(createObjectInstantiator(entity, defaultConstructor), + entity.getPersistenceConstructor()); + } + } + + return super.doCreateEntityInstantiator(entity); + } + + /** + * Resolves a {@link PreferredConstructor} to a synthetic Kotlin constructor accepting the same user-space parameters + * suffixed by Kotlin-specifics required for defaulting and the {@code kotlin.jvm.internal.DefaultConstructorMarker}. + * + * @since 2.0 + * @author Mark Paluch + */ + static class DefaultingKotlinConstructorResolver { + + @Nullable private final PreferredConstructor defaultConstructor; + + @SuppressWarnings("unchecked") + DefaultingKotlinConstructorResolver(PersistentEntity entity) { + + Constructor hit = resolveDefaultConstructor(entity); + PreferredConstructor persistenceConstructor = entity.getPersistenceConstructor(); + + if (hit != null && persistenceConstructor != null) { + this.defaultConstructor = new PreferredConstructor<>(hit, + persistenceConstructor.getParameters().toArray(new Parameter[0])); + } else { + this.defaultConstructor = null; + } + } + + @Nullable + private static Constructor resolveDefaultConstructor(PersistentEntity entity) { + + if (entity.getPersistenceConstructor() == null) { + return null; + } + + Constructor hit = null; + Constructor constructor = entity.getPersistenceConstructor().getConstructor(); + + for (Constructor candidate : entity.getType().getDeclaredConstructors()) { + + // use only synthetic constructors + if (!candidate.isSynthetic()) { + continue; + } + + // with a parameter count greater zero + if (constructor.getParameterCount() == 0) { + continue; + } + + // candidates must contain at least two additional parameters (int, DefaultConstructorMarker) + if (constructor.getParameterCount() + 2 > candidate.getParameterCount()) { + continue; + } + + java.lang.reflect.Parameter[] constructorParameters = constructor.getParameters(); + java.lang.reflect.Parameter[] candidateParameters = candidate.getParameters(); + + if (!candidateParameters[candidateParameters.length - 1].getType().getName() + .equals("kotlin.jvm.internal.DefaultConstructorMarker")) { + continue; + } + + if (parametersMatch(constructorParameters, candidateParameters)) { + hit = candidate; + break; + } + } + + return hit; + } + + private static boolean parametersMatch(java.lang.reflect.Parameter[] constructorParameters, + java.lang.reflect.Parameter[] candidateParameters) { + + return IntStream.range(0, constructorParameters.length) + .allMatch(i -> constructorParameters[i].getType().equals(candidateParameters[i].getType())); + } + + @Nullable + PreferredConstructor getDefaultConstructor() { + return defaultConstructor; + } + } + + /** + * Entity instantiator for Kotlin constructors that apply parameter defaulting. Kotlin constructors that apply + * argument defaulting are marked with {@link kotlin.jvm.internal.DefaultConstructorMarker} and accept additional + * parameters besides the regular (user-space) parameters. Additional parameters are: + *
    + *
  • defaulting bitmask ({@code int}), a bit mask slot for each 32 parameters
  • + *
  • {@code DefaultConstructorMarker} (usually null)
  • + *
+ * Defaulting bitmask + *

+ * The defaulting bitmask is a 32 bit integer representing which positional argument should be defaulted. Defaulted + * arguments are passed as {@literal null} and require the appropriate positional bit set ( {@code 1 << 2} for the 2. + * argument)). Since the bitmask represents only 32 bit states, it requires additional masks (slots) if more than 32 + * arguments are represented. + * + * @author Mark Paluch + * @since 2.0 + */ + static class DefaultingKotlinClassInstantiatorAdapter implements EntityInstantiator { + + private final ObjectInstantiator instantiator; + private final List kParameters; + private final Constructor synthetic; + + DefaultingKotlinClassInstantiatorAdapter(ObjectInstantiator instantiator, PreferredConstructor constructor) { + + KFunction kotlinConstructor = ReflectJvmMapping.getKotlinFunction(constructor.getConstructor()); + + if (kotlinConstructor == null) { + throw new IllegalArgumentException( + "No corresponding Kotlin constructor found for " + constructor.getConstructor()); + } + + this.instantiator = instantiator; + this.kParameters = kotlinConstructor.getParameters(); + this.synthetic = constructor.getConstructor(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.convert.EntityInstantiator#createInstance(org.springframework.data.mapping.PersistentEntity, org.springframework.data.mapping.model.ParameterValueProvider) + */ + @Override + @SuppressWarnings("unchecked") + public , P extends PersistentProperty

> T createInstance(E entity, + ParameterValueProvider

provider) { + + PreferredConstructor preferredConstructor = entity.getPersistenceConstructor(); + Assert.notNull(preferredConstructor, "PreferredConstructor must not be null!"); + + int[] defaulting = new int[(synthetic.getParameterCount() / 32) + 1]; + + Object[] params = allocateArguments( + synthetic.getParameterCount() + defaulting.length + /* DefaultConstructorMarker */1); + int userParameterCount = kParameters.size(); + + List> parameters = preferredConstructor.getParameters(); + + // Prepare user-space arguments + for (int i = 0; i < userParameterCount; i++) { + + int slot = i / 32; + int offset = slot * 32; + + Object param = provider.getParameterValue(parameters.get(i)); + + KParameter kParameter = kParameters.get(i); + + // what about null and parameter is mandatory? What if parameter is non-null? + if (kParameter.isOptional()) { + + if (param == null) { + defaulting[slot] = defaulting[slot] | (1 << (i - offset)); + } + } + + params[i] = param; + } + + // append nullability masks to creation arguments + for (int i = 0; i < defaulting.length; i++) { + params[userParameterCount + i] = defaulting[i]; + } + + try { + return (T) instantiator.newInstance(params); + } finally { + deallocateArguments(params); + } + } + } +} diff --git a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java index d0d828eed..144894528 100644 --- a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java @@ -20,34 +20,12 @@ import lombok.RequiredArgsConstructor; import java.io.Serializable; import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; +import java.util.*; import java.util.stream.Collectors; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.annotation.TypeAlias; -import org.springframework.data.mapping.Alias; -import org.springframework.data.mapping.Association; -import org.springframework.data.mapping.AssociationHandler; -import org.springframework.data.mapping.IdentifierAccessor; -import org.springframework.data.mapping.MappingException; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.PreferredConstructor; -import org.springframework.data.mapping.PropertyHandler; -import org.springframework.data.mapping.SimpleAssociationHandler; -import org.springframework.data.mapping.SimplePropertyHandler; -import org.springframework.data.mapping.TargetAwareIdentifierAccessor; +import org.springframework.data.mapping.*; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -112,7 +90,7 @@ public class BasicPersistentEntity> implement this.properties = new ArrayList<>(); this.persistentPropertiesCache = new ArrayList<>(); this.comparator = comparator; - this.constructor = new PreferredConstructorDiscoverer<>(this).getConstructor(); + this.constructor = PreferredConstructorDiscoverer.discover(this); this.associations = comparator == null ? new HashSet<>() : new TreeSet<>(new AssociationComparator<>(comparator)); this.propertyCache = new HashMap<>(); diff --git a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java index eb8ed3fcf..a71cac255 100644 --- a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java +++ b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java @@ -15,20 +15,14 @@ */ package org.springframework.data.mapping.model; -import java.lang.annotation.Annotation; -import java.lang.reflect.Constructor; -import java.util.List; - -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.ParameterNameDiscoverer; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PreferredConstructor; -import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.data.util.ClassTypeInformation; -import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; +import com.mysema.commons.lang.Assert; + /** * Helper class to find a {@link PreferredConstructor}. * @@ -37,108 +31,34 @@ import org.springframework.lang.Nullable; * @author Roman Rodov * @author Mark Paluch */ -public class PreferredConstructorDiscoverer> { - - private final ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); - - private @Nullable PreferredConstructor constructor; +public interface PreferredConstructorDiscoverer> { /** - * Creates a new {@link PreferredConstructorDiscoverer} for the given type. + * Discovers the {@link PreferredConstructor} for the given type. * * @param type must not be {@literal null}. - */ - public PreferredConstructorDiscoverer(Class type) { - this(ClassTypeInformation.from(type), null); - } - - /** - * Creates a new {@link PreferredConstructorDiscoverer} for the given {@link PersistentEntity}. - * - * @param entity must not be {@literal null}. - */ - public PreferredConstructorDiscoverer(PersistentEntity entity) { - this(entity.getTypeInformation(), entity); - } - - /** - * Creates a new {@link PreferredConstructorDiscoverer} for the given type. - * - * @param type must not be {@literal null}. - * @param entity - */ - protected PreferredConstructorDiscoverer(TypeInformation type, @Nullable PersistentEntity entity) { - - boolean noArgConstructorFound = false; - int numberOfArgConstructors = 0; - Class rawOwningType = type.getType(); - - for (Constructor candidate : rawOwningType.getDeclaredConstructors()) { - - PreferredConstructor preferredConstructor = buildPreferredConstructor(candidate, type, entity); - - // Synthetic constructors should not be considered - if (preferredConstructor.getConstructor().isSynthetic()) { - continue; - } - - // Explicitly defined constructor trumps all - if (preferredConstructor.isExplicitlyAnnotated()) { - this.constructor = preferredConstructor; - return; - } - - // No-arg constructor trumps custom ones - if (this.constructor == null || preferredConstructor.isNoArgConstructor()) { - this.constructor = preferredConstructor; - } - - if (preferredConstructor.isNoArgConstructor()) { - noArgConstructorFound = true; - } else { - numberOfArgConstructors++; - } - } - - if (!noArgConstructorFound && numberOfArgConstructors > 1) { - this.constructor = null; - } - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private PreferredConstructor buildPreferredConstructor(Constructor constructor, - TypeInformation typeInformation, @Nullable PersistentEntity entity) { - - List> parameterTypes = typeInformation.getParameterTypes(constructor); - - if (parameterTypes.isEmpty()) { - return new PreferredConstructor<>((Constructor) constructor); - } - - String[] parameterNames = discoverer.getParameterNames(constructor); - - Parameter[] parameters = new Parameter[parameterTypes.size()]; - Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); - - for (int i = 0; i < parameterTypes.size(); i++) { - - String name = parameterNames == null ? null : parameterNames[i]; - TypeInformation type = parameterTypes.get(i); - Annotation[] annotations = parameterAnnotations[i]; - - parameters[i] = new Parameter(name, type, annotations, entity); - } - - return new PreferredConstructor<>((Constructor) constructor, parameters); - } - - /** - * Returns the discovered {@link PreferredConstructor}. - * - * @return + * @return the {@link PreferredConstructor} if found or {@literal null}. */ @Nullable - public PreferredConstructor getConstructor() { - return constructor; + static > PreferredConstructor discover(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + return PreferredConstructorDiscoverers.findDiscoverer(type).discover(ClassTypeInformation.from(type), null); + } + + /** + * Discovers the {@link PreferredConstructorDiscoverer} for the given {@link PersistentEntity}. + * + * @param entity must not be {@literal null}. + * @return the {@link PreferredConstructor} if found or {@literal null}. + */ + @Nullable + static > PreferredConstructor discover(PersistentEntity entity) { + + Assert.notNull(entity, "PersistentEntity must not be null!"); + + return PreferredConstructorDiscoverers.findDiscoverer(entity.getType()).discover(entity.getTypeInformation(), + entity); } } diff --git a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverers.java b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverers.java new file mode 100644 index 000000000..9fbafb58d --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverers.java @@ -0,0 +1,183 @@ +/* + * Copyright 2011-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mapping.model; + +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KFunction; +import kotlin.reflect.full.KClasses; +import kotlin.reflect.jvm.ReflectJvmMapping; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * Helper class to find a {@link PreferredConstructor}. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @author Roman Rodov + * @author Mark Paluch + * @since 2.0 + */ +enum PreferredConstructorDiscoverers { + + /** + * Discovers a {@link PreferredConstructor} for Java types. + */ + DEFAULT { + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.PreferredConstructorDiscoverers#discover(org.springframework.data.util.TypeInformation, org.springframework.data.mapping.PersistentEntity) + */ + @Nullable + @Override + public > PreferredConstructor discover(TypeInformation type, + @Nullable PersistentEntity entity) { + + boolean noArgConstructorFound = false; + int numberOfArgConstructors = 0; + Class rawOwningType = type.getType(); + PreferredConstructor constructor = null; + + for (Constructor candidate : rawOwningType.getDeclaredConstructors()) { + + PreferredConstructor preferredConstructor = buildPreferredConstructor(candidate, type, entity); + + // Synthetic constructors should not be considered + if (preferredConstructor.getConstructor().isSynthetic()) { + continue; + } + + // Explicitly defined constructor trumps all + if (preferredConstructor.isExplicitlyAnnotated()) { + return preferredConstructor; + } + + // No-arg constructor trumps custom ones + if (constructor == null || preferredConstructor.isNoArgConstructor()) { + constructor = preferredConstructor; + } + + if (preferredConstructor.isNoArgConstructor()) { + noArgConstructorFound = true; + } else { + numberOfArgConstructors++; + } + } + + if (!noArgConstructorFound && numberOfArgConstructors > 1) { + constructor = null; + } + + return constructor; + } + }, + + /** + * Discovers a {@link PreferredConstructor} for Kotlin types. + */ + KOTLIN { + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.PreferredConstructorDiscoverers#discover(org.springframework.data.util.TypeInformation, org.springframework.data.mapping.PersistentEntity) + */ + @Nullable + @Override + public > PreferredConstructor discover(TypeInformation type, + @Nullable PersistentEntity entity) { + + Class rawOwningType = type.getType(); + + return Arrays.stream(rawOwningType.getDeclaredConstructors()) // + .map(it -> buildPreferredConstructor(it, type, entity)) // + .filter(it -> !it.getConstructor().isSynthetic()) // Synthetic constructors should not be considered + .filter(PreferredConstructor::isExplicitlyAnnotated) // Explicitly defined constructor trumps all + .findFirst() // + .orElseGet(() -> { + + KFunction primaryConstructor = KClasses + .getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(type.getType())); + Constructor javaConstructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor); + + return javaConstructor != null ? buildPreferredConstructor(javaConstructor, type, entity) : null; + }); + } + }; + + private static final ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); + + /** + * Find the appropriate discoverer for {@code type}. + * + * @param type must not be {@literal null}. + * @return the appropriate discoverer for {@code type}. + */ + public static PreferredConstructorDiscoverers findDiscoverer(Class type) { + return ReflectionUtils.isKotlinClass(type) ? KOTLIN : DEFAULT; + } + + /** + * Discovers a constructor for the given type. + * + * @param type must not be {@literal null}. + * @param entity may be {@literal null}. + * @return the {@link PreferredConstructor} if found or {@literal null}. + */ + @Nullable + public abstract > PreferredConstructor discover(TypeInformation type, + @Nullable PersistentEntity entity); + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static > PreferredConstructor buildPreferredConstructor( + Constructor constructor, TypeInformation typeInformation, @Nullable PersistentEntity entity) { + + List> parameterTypes = typeInformation.getParameterTypes(constructor); + + if (parameterTypes.isEmpty()) { + return new PreferredConstructor<>((Constructor) constructor); + } + + String[] parameterNames = discoverer.getParameterNames(constructor); + + Parameter[] parameters = new Parameter[parameterTypes.size()]; + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + + for (int i = 0; i < parameterTypes.size(); i++) { + + String name = parameterNames == null ? null : parameterNames[i]; + TypeInformation type = parameterTypes.get(i); + Annotation[] annotations = parameterAnnotations[i]; + + parameters[i] = new Parameter(name, type, annotations, entity); + } + + return new PreferredConstructor<>((Constructor) constructor, parameters); + } +} diff --git a/src/main/java/org/springframework/data/repository/query/ReturnedType.java b/src/main/java/org/springframework/data/repository/query/ReturnedType.java index aae5ac89b..7921ec18b 100644 --- a/src/main/java/org/springframework/data/repository/query/ReturnedType.java +++ b/src/main/java/org/springframework/data/repository/query/ReturnedType.java @@ -45,6 +45,7 @@ import org.springframework.util.ConcurrentReferenceHashMap; * * @author Oliver Gierke * @author Christoph Strobl + * @author Mark Paluch * @since 1.12 */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @@ -296,8 +297,7 @@ public abstract class ReturnedType { return Collections.emptyList(); } - PreferredConstructorDiscoverer discoverer = new PreferredConstructorDiscoverer(type); - PreferredConstructor constructor = discoverer.getConstructor(); + PreferredConstructor constructor = PreferredConstructorDiscoverer.discover(type); if (constructor == null) { return Collections.emptyList(); diff --git a/src/main/java/org/springframework/data/util/ReflectionUtils.java b/src/main/java/org/springframework/data/util/ReflectionUtils.java index 726a48732..c719a3027 100644 --- a/src/main/java/org/springframework/data/util/ReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/ReflectionUtils.java @@ -44,11 +44,15 @@ import org.springframework.util.ReflectionUtils.FieldFilter; * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl + * @author Mark Paluch * @since 1.5 */ @UtilityClass public class ReflectionUtils { + private static final boolean KOTLIN_IS_PRESENT = ClassUtils.isPresent("kotlin.Unit", + BeanUtils.class.getClassLoader()); + /** * Creates an instance of the class with the given fully qualified name or returns the given default instance if the * class cannot be loaded or instantiated. @@ -338,4 +342,17 @@ public class ReflectionUtils { return true; } + + /** + * Return true if the specified class is a Kotlin one. + * + * @return {@literal true} if {@code type} is a Kotlin class. + * @since 2.0 + */ + public static boolean isKotlinClass(Class type) { + + return KOTLIN_IS_PRESENT && Arrays.stream(type.getDeclaredAnnotations()) // + .map(Annotation::annotationType) // + .anyMatch(annotation -> annotation.getName().equals("kotlin.Metadata")); + } } diff --git a/src/test/java/org/springframework/data/convert/ClassGeneratingEntityInstantiatorUnitTests.java b/src/test/java/org/springframework/data/convert/ClassGeneratingEntityInstantiatorUnitTests.java index 1b63ab5a3..5a578b875 100755 --- a/src/test/java/org/springframework/data/convert/ClassGeneratingEntityInstantiatorUnitTests.java +++ b/src/test/java/org/springframework/data/convert/ClassGeneratingEntityInstantiatorUnitTests.java @@ -53,8 +53,6 @@ public class ClassGeneratingEntityInstantiatorUnitTests

entity; @Mock ParameterValueProvider

provider; - @Mock PreferredConstructor constructor; - @Mock Parameter parameter; @Test public void instantiatesSimpleObjectCorrectly() { @@ -72,11 +70,10 @@ public class ClassGeneratingEntityInstantiatorUnitTests

constructor = new PreferredConstructorDiscoverer(Foo.class) - .getConstructor(); + PreferredConstructor constructor = PreferredConstructorDiscoverer.discover(Foo.class); doReturn(Foo.class).when(entity).getType(); doReturn(constructor).when(entity).getPersistenceConstructor(); @@ -150,23 +147,21 @@ public class ClassGeneratingEntityInstantiatorUnitTests

(ObjCtorDefault.class).getConstructor())// + doReturn(PreferredConstructorDiscoverer.discover(ObjCtorDefault.class))// .when(entity).getPersistenceConstructor(); IntStream.range(0, 2).forEach(i -> assertThat(this.instance.createInstance(entity, provider)).isInstanceOf(ObjCtorDefault.class)); } - @Test // DATACMNS-578 - @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test // DATACMNS-578, DATACMNS-1126 public void instantiateObjCtorNoArgs() { doReturn(ObjCtorNoArgs.class).when(entity).getType(); - doReturn(new PreferredConstructorDiscoverer<>(ObjCtorNoArgs.class).getConstructor())// + doReturn(PreferredConstructorDiscoverer.discover(ObjCtorNoArgs.class))// .when(entity).getPersistenceConstructor(); IntStream.range(0, 2).forEach(i -> { @@ -178,12 +173,11 @@ public class ClassGeneratingEntityInstantiatorUnitTests

(ObjCtor1ParamString.class).getConstructor())// + doReturn(PreferredConstructorDiscoverer.discover(ObjCtor1ParamString.class))// .when(entity).getPersistenceConstructor(); doReturn("FOO").when(provider).getParameterValue(any()); @@ -197,12 +191,11 @@ public class ClassGeneratingEntityInstantiatorUnitTests

(ObjCtor2ParamStringString.class).getConstructor())// + doReturn(PreferredConstructorDiscoverer.discover(ObjCtor2ParamStringString.class))// .when(entity).getPersistenceConstructor(); IntStream.range(0, 2).forEach(i -> { @@ -218,12 +211,11 @@ public class ClassGeneratingEntityInstantiatorUnitTests

(ObjectCtor1ParamInt.class).getConstructor())// + doReturn(PreferredConstructorDiscoverer.discover(ObjectCtor1ParamInt.class))// .when(entity).getPersistenceConstructor(); IntStream.range(0, 2).forEach(i -> { @@ -237,12 +229,12 @@ public class ClassGeneratingEntityInstantiatorUnitTests

(ObjectCtor7ParamsString5IntsString.class).getConstructor())// + doReturn(PreferredConstructorDiscoverer.discover(ObjectCtor7ParamsString5IntsString.class))// .when(entity).getPersistenceConstructor(); IntStream.range(0, 2).forEach(i -> { diff --git a/src/test/java/org/springframework/data/convert/ReflectionEntityInstantiatorUnitTests.java b/src/test/java/org/springframework/data/convert/ReflectionEntityInstantiatorUnitTests.java index 1fade6e3b..355e3f178 100755 --- a/src/test/java/org/springframework/data/convert/ReflectionEntityInstantiatorUnitTests.java +++ b/src/test/java/org/springframework/data/convert/ReflectionEntityInstantiatorUnitTests.java @@ -51,8 +51,6 @@ public class ReflectionEntityInstantiatorUnitTests

entity; @Mock ParameterValueProvider

provider; - @Mock PreferredConstructor constructor; - @Mock Parameter parameter; @Test public void instantiatesSimpleObjectCorrectly() { @@ -68,10 +66,10 @@ public class ReflectionEntityInstantiatorUnitTests

constructor = new PreferredConstructorDiscoverer(Foo.class).getConstructor(); + PreferredConstructor constructor = PreferredConstructorDiscoverer.discover(Foo.class); doReturn(constructor).when(entity).getPersistenceConstructor(); diff --git a/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java b/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java index 58a003437..40f1ed324 100755 --- a/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java @@ -36,13 +36,10 @@ import org.springframework.data.util.ClassTypeInformation; */ public class PreferredConstructorDiscovererUnitTests

> { - @Test + @Test // DATACMNS-1126 public void findsNoArgConstructorForClassWithoutExplicitConstructor() { - PreferredConstructorDiscoverer discoverer = new PreferredConstructorDiscoverer<>( - EntityWithoutConstructor.class); - - assertThat(discoverer.getConstructor()).satisfies(constructor -> { + assertThat(PreferredConstructorDiscoverer.discover(EntityWithoutConstructor.class)).satisfies(constructor -> { assertThat(constructor).isNotNull(); assertThat(constructor.isNoArgConstructor()).isTrue(); @@ -50,13 +47,10 @@ public class PreferredConstructorDiscovererUnitTests

discoverer = new PreferredConstructorDiscoverer<>( - ClassWithEmptyConstructor.class); - - assertThat(discoverer.getConstructor()).satisfies(constructor -> { + assertThat(PreferredConstructorDiscoverer.discover(ClassWithEmptyConstructor.class)).satisfies(constructor -> { assertThat(constructor).isNotNull(); assertThat(constructor.isNoArgConstructor()).isTrue(); @@ -64,22 +58,19 @@ public class PreferredConstructorDiscovererUnitTests

discoverer = new PreferredConstructorDiscoverer<>( - ClassWithMultipleConstructorsWithoutEmptyOne.class); - - assertThat(discoverer.getConstructor()).isNull(); + assertThat(PreferredConstructorDiscoverer.discover(ClassWithMultipleConstructorsWithoutEmptyOne.class)).isNull(); } - @Test + @Test // DATACMNS-1126 + @SuppressWarnings({ "unchecked", "rawtypes" }) public void usesConstructorWithAnnotationOverEveryOther() { - PreferredConstructorDiscoverer discoverer = new PreferredConstructorDiscoverer<>( - ClassWithMultipleConstructorsAndAnnotation.class); - assertThat(discoverer.getConstructor()).satisfies(constructor -> { + assertThat(PreferredConstructorDiscoverer.discover(ClassWithMultipleConstructorsAndAnnotation.class)) + .satisfies(constructor -> { assertThat(constructor).isNotNull(); assertThat(constructor.isNoArgConstructor()).isFalse(); @@ -87,7 +78,7 @@ public class PreferredConstructorDiscovererUnitTests

> parameters = constructor.getParameters().iterator(); + Iterator> parameters = (Iterator) constructor.getParameters().iterator(); Parameter parameter = parameters.next(); assertThat(parameter.getType().getType()).isEqualTo(Long.class); @@ -95,26 +86,25 @@ public class PreferredConstructorDiscovererUnitTests

entity = new BasicPersistentEntity<>(ClassTypeInformation.from(Inner.class)); - PreferredConstructorDiscoverer discoverer = new PreferredConstructorDiscoverer<>(entity); - assertThat(discoverer.getConstructor()).satisfies(constructor -> { + assertThat(PreferredConstructorDiscoverer.discover(entity)).satisfies(constructor -> { Parameter parameter = constructor.getParameters().iterator().next(); assertThat(constructor.isEnclosingClassParameter(parameter)).isTrue(); }); } - @Test // DATACMNS-1082 + @Test // DATACMNS-1082, DATACMNS-1126 public void skipsSyntheticConstructor() { - PersistentEntity entity = new BasicPersistentEntity<>(ClassTypeInformation.from(SyntheticConstructor.class)); - PreferredConstructorDiscoverer discoverer = new PreferredConstructorDiscoverer<>(entity); + PersistentEntity entity = new BasicPersistentEntity<>( + ClassTypeInformation.from(SyntheticConstructor.class)); - assertThat(discoverer.getConstructor()).satisfies(constructor -> { + assertThat(PreferredConstructorDiscoverer.discover(entity)).satisfies(constructor -> { PersistenceConstructor annotation = constructor.getConstructor().getAnnotation(PersistenceConstructor.class); assertThat(annotation).isNotNull(); @@ -124,8 +114,7 @@ public class PreferredConstructorDiscovererUnitTests

+ @Mock lateinit var provider: ParameterValueProvider + + @Test // DATACMNS-1126 + fun `should create instance`() { + + val entity = this.entity as PersistentEntity + val constructor = PreferredConstructorDiscoverer.discover(Contact::class.java) + + doReturn("Walter", "White").`when`(provider).getParameterValue(any()) + doReturn(constructor).whenever(entity).persistenceConstructor + doReturn(constructor.constructor.declaringClass).whenever(entity).type + + val instance: Contact = ClassGeneratingKotlinEntityInstantiator().createInstance(entity, provider) + + Assertions.assertThat(instance.firstname).isEqualTo("Walter") + Assertions.assertThat(instance.lastname).isEqualTo("White") + } + + @Test // DATACMNS-1126 + fun `should create instance and fill in defaults`() { + + val entity = this.entity as PersistentEntity + val constructor = PreferredConstructorDiscoverer.discover(ContactWithDefaulting::class.java) + + doReturn("Walter", null, "Skyler", null, null, null, null, null, null, null, /* 0-9 */ + null, null, null, null, null, null, null, null, null, null, /* 10-19 */ + null, null, null, null, null, null, null, null, null, null, /* 20-29 */ + null, "Walter", null, "Junior", null).`when`(provider).getParameterValue(any()) + doReturn(constructor).whenever(entity).persistenceConstructor + doReturn(constructor.constructor.declaringClass).whenever(entity).type + + val instance: ContactWithDefaulting = ClassGeneratingKotlinEntityInstantiator().createInstance(entity, provider) + + Assertions.assertThat(instance.prop0).isEqualTo("Walter") + Assertions.assertThat(instance.prop2).isEqualTo("Skyler") + Assertions.assertThat(instance.prop31).isEqualTo("Walter") + Assertions.assertThat(instance.prop32).isEqualTo("White") + Assertions.assertThat(instance.prop33).isEqualTo("Junior") + Assertions.assertThat(instance.prop34).isEqualTo("White") + } + + data class Contact(val firstname: String, val lastname: String) + + data class ContactWithDefaulting(val prop0: String, val prop1: String = "White", val prop2: String, + val prop3: String = "White", val prop4: String = "White", val prop5: String = "White", + val prop6: String = "White", val prop7: String = "White", val prop8: String = "White", + val prop9: String = "White", val prop10: String = "White", val prop11: String = "White", + val prop12: String = "White", val prop13: String = "White", val prop14: String = "White", + val prop15: String = "White", val prop16: String = "White", val prop17: String = "White", + val prop18: String = "White", val prop19: String = "White", val prop20: String = "White", + val prop21: String = "White", val prop22: String = "White", val prop23: String = "White", + val prop24: String = "White", val prop25: String = "White", val prop26: String = "White", + val prop27: String = "White", val prop28: String = "White", val prop29: String = "White", + val prop30: String = "White", val prop31: String = "White", val prop32: String = "White", + val prop33: String, val prop34: String = "White" + ) +} + diff --git a/src/test/kotlin/org/springframework/data/convert/ReflectionEntityInstantiatorDataClassUnitTests.kt b/src/test/kotlin/org/springframework/data/convert/ReflectionEntityInstantiatorDataClassUnitTests.kt new file mode 100644 index 000000000..ce6d06b57 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/convert/ReflectionEntityInstantiatorDataClassUnitTests.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.convert + +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import org.assertj.core.api.Assertions +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.springframework.data.mapping.PersistentEntity +import org.springframework.data.mapping.context.SamplePersistentProperty +import org.springframework.data.mapping.model.ParameterValueProvider +import org.springframework.data.mapping.model.PreferredConstructorDiscoverer + +/** + * Unit tests for [ReflectionEntityInstantiator] creating instances using Kotlin data classes. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner::class) +@Suppress("UNCHECKED_CAST") +class ReflectionEntityInstantiatorDataClassUnitTests { + + @Mock lateinit var entity: PersistentEntity<*, *> + @Mock lateinit var provider: ParameterValueProvider + + @Test // DATACMNS-1126 + fun `should create instance`() { + + val entity = this.entity as PersistentEntity + val constructor = PreferredConstructorDiscoverer.discover(Contact::class.java) + + doReturn("Walter", "White").`when`(provider).getParameterValue(any()) + doReturn(constructor).whenever(entity).persistenceConstructor + + val instance: Contact = ReflectionEntityInstantiator.INSTANCE.createInstance(entity, provider) + + Assertions.assertThat(instance.firstname).isEqualTo("Walter") + Assertions.assertThat(instance.lastname).isEqualTo("White") + } + + @Test // DATACMNS-1126 + fun `should create instance and fill in defaults`() { + + val entity = this.entity as PersistentEntity + val constructor = PreferredConstructorDiscoverer.discover(ContactWithDefaulting::class.java) + + doReturn("Walter", null).`when`(provider).getParameterValue(any()) + doReturn(constructor).whenever(entity).persistenceConstructor + + val instance: ContactWithDefaulting = ReflectionEntityInstantiator.INSTANCE.createInstance(entity, provider) + + Assertions.assertThat(instance.firstname).isEqualTo("Walter") + Assertions.assertThat(instance.lastname).isEqualTo("White") + } + + data class Contact(val firstname: String, val lastname: String) + + data class ContactWithDefaulting(val firstname: String, val lastname: String = "White") +} + diff --git a/src/test/kotlin/org/springframework/data/mapping/model/PreferredConstructorDiscovererUnitTests.kt b/src/test/kotlin/org/springframework/data/mapping/model/PreferredConstructorDiscovererUnitTests.kt new file mode 100644 index 000000000..a21c83ddd --- /dev/null +++ b/src/test/kotlin/org/springframework/data/mapping/model/PreferredConstructorDiscovererUnitTests.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mapping.model + +import org.assertj.core.api.Assertions +import org.junit.Test +import org.springframework.data.annotation.PersistenceConstructor +import org.springframework.data.mapping.model.AbstractPersistentPropertyUnitTests.* + +/** + * Unit tests for [PreferredConstructorDiscoverer]. + * + * @author Mark Paluch + */ +class PreferredConstructorDiscovererUnitTests { + + @Test // DATACMNS-1126 + fun `should discover simple constructor`() { + + val constructor = PreferredConstructorDiscoverer.discover(Simple::class.java) + + Assertions.assertThat(constructor.parameters.size).isEqualTo(1) + } + + @Test // DATACMNS-1126 + fun `should reject two constructors`() { + + val constructor = PreferredConstructorDiscoverer.discover(TwoConstructors::class.java) + + Assertions.assertThat(constructor.parameters.size).isEqualTo(1) + } + + @Test // DATACMNS-1126 + fun `should discover annotated constructor`() { + + val constructor = PreferredConstructorDiscoverer.discover(AnnotatedConstructors::class.java) + + Assertions.assertThat(constructor.parameters.size).isEqualTo(2) + } + + @Test // DATACMNS-1126 + fun `should discover default constructor`() { + + val constructor = PreferredConstructorDiscoverer.discover(DefaultConstructor::class.java) + + Assertions.assertThat(constructor.parameters.size).isEqualTo(1) + } + + @Test // DATACMNS-1126 + fun `should discover default annotated constructor`() { + + val constructor = PreferredConstructorDiscoverer.discover(TwoDefaultConstructorsAnnotated::class.java) + + Assertions.assertThat(constructor.parameters.size).isEqualTo(3) + } + + data class Simple(val firstname: String) + + class TwoConstructors(val firstname: String) { + constructor(firstname: String, lastname: String) : this(firstname) + } + + class AnnotatedConstructors(val firstname: String) { + + @PersistenceConstructor + constructor(firstname: String, lastname: String) : this(firstname) + } + + class DefaultConstructor(val firstname: String = "foo") { + } + + class TwoDefaultConstructorsAnnotated(val firstname: String = "foo", val lastname: String = "bar") { + + @PersistenceConstructor + constructor(firstname: String = "foo", lastname: String = "bar", age: Int) : this(firstname, lastname) + } +} \ No newline at end of file