From cd2b309f9bd4aacbf2371070397cc64e39fe578a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 26 Jul 2017 12:07:20 +0200 Subject: [PATCH] DATACMNS-1126 - Add Kotlin constructor support. 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. ClassGeneratingEntityInstantiator 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) } --- pom.xml | 43 +++ .../ClassGeneratingEntityInstantiator.java | 252 ++++++++++++++++-- .../model/PreferredConstructorDiscoverer.java | 33 +++ .../data/util/ReflectionUtils.java | 49 ++-- ...ingEntityInstantiatorDataClassUnitTests.kt | 98 +++++++ ...ionEntityInstantiatorDataClassUnitTests.kt | 77 ++++++ ...PreferredConstructorDiscovererUnitTests.kt | 90 +++++++ 7 files changed, 611 insertions(+), 31 deletions(-) create mode 100644 src/test/kotlin/org/springframework/data/convert/ClassGeneratingEntityInstantiatorDataClassUnitTests.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 779ba95083..d27b5a8d80 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 deed67e841..5a4a23222c 100644 --- a/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java +++ b/src/main/java/org/springframework/data/convert/ClassGeneratingEntityInstantiator.java @@ -17,6 +17,10 @@ import static org.springframework.asm.Opcodes.*; +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.jvm.ReflectJvmMapping; + import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.security.AccessController; @@ -26,6 +30,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.IntStream; import org.springframework.asm.ClassWriter; import org.springframework.asm.MethodVisitor; @@ -37,6 +42,7 @@ import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.data.mapping.model.MappingInstantiationException; import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -47,6 +53,8 @@ * {@link PersistentEntity}'s {@link PreferredConstructor} to instantiate an instance of the entity by dynamically * generating factory methods with appropriate constructor invocations via ASM. If we cannot generate byte code for a * type, we gracefully fall-back to the {@link ReflectionEntityInstantiator}. + *

+ * Adopts to Kotlin constructors using parameter defaulting. * * @author Thomas Darimont * @author Oliver Gierke @@ -57,6 +65,19 @@ */ 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,7 +141,19 @@ private EntityInstantiator createEntityInstantiator(PersistentEntity entit } try { - return new EntityInstantiatorAdapter(createObjectInstantiator(entity)); + + if (ReflectionUtils.isKotlinClass(entity.getType())) { + + PreferredConstructor defaultConstructor = new DefaultingKotlinConstructorResolver(entity) + .getDefaultConstructor(); + + if (defaultConstructor != null) { + return new DefaultingKotlinClassEntityInstantiator(createObjectInstantiator(entity, defaultConstructor), + defaultConstructor); + } + } + + return new EntityInstantiatorAdapter(createObjectInstantiator(entity, entity.getPersistenceConstructor())); } catch (Throwable ex) { return ReflectionEntityInstantiator.INSTANCE; } @@ -151,22 +184,28 @@ private boolean shouldUseReflectionEntityInstantiator(PersistentEntity ent } /** - * Creates a dynamically generated {@link ObjectInstantiator} for the given {@link PersistentEntity}. There will - * always be exactly one {@link ObjectInstantiator} instance per {@link PersistentEntity}. - *

+ * 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) { + private 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); } } + private static Object[] allocateArguments(int argumentCount) { + return argumentCount < ARG_CACHE_SIZE ? OBJECT_POOL.get()[argumentCount] : new Object[argumentCount]; + } + /** * Adapter to forward an invocation of the {@link EntityInstantiator} API to an {@link ObjectInstantiator}. * @@ -216,7 +255,7 @@ public , P extends PersistentPrope private

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

provider) { - if (provider == null || constructor == null || !constructor.hasParameters()) { + if (constructor == null || !constructor.hasParameters()) { return EMPTY_ARRAY; } @@ -230,6 +269,185 @@ private

, T> Object[] extractInvocationArguments } } + /** + * 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 + *

+ * 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 DefaultingKotlinClassEntityInstantiator implements EntityInstantiator { + + private final ObjectInstantiator instantiator; + private final List kParameters; + private final Constructor synthetic; + private final int optionalParameterCount; + + DefaultingKotlinClassEntityInstantiator(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(); + this.optionalParameterCount = Math.toIntExact(kParameters.stream().filter(KParameter::isOptional).count()); + } + + /* + * (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[(optionalParameterCount / 32) + 1]; + int optionalParameter = 0; + + 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 = optionalParameter / 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 << (optionalParameter - offset)); + } + + optionalParameter++; + } + + 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 { + Arrays.fill(params, null); + } + } + } + /** * Needs to be public as otherwise the implementation class generated does not see the interface from the classloader. * @@ -290,7 +508,7 @@ static class ObjectInstantiatorClassGenerator { private final ByteArrayClassLoader classLoader; - private ObjectInstantiatorClassGenerator() { + ObjectInstantiatorClassGenerator() { this.classLoader = AccessController.doPrivileged( (PrivilegedAction) () -> new ByteArrayClassLoader(ClassUtils.getDefaultClassLoader())); @@ -300,12 +518,14 @@ private ObjectInstantiatorClassGenerator() { * 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 +543,11 @@ private String generateClassName(PersistentEntity entity) { * * @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 +556,7 @@ public byte[] generateBytecode(String internalClassName, PersistentEntity visitDefaultConstructor(cw); - visitCreateMethod(cw, entity); + visitCreateMethod(cw, entity, constructor); cw.visitEnd(); @@ -357,8 +579,10 @@ private void visitDefaultConstructor(ClassWriter cw) { * * @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 +592,6 @@ private void visitCreateMethod(ClassWriter cw, PersistentEntity entity) { 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/mapping/model/PreferredConstructorDiscoverer.java b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java index eb8ed3fcfd..17efcb4faf 100644 --- a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java +++ b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java @@ -15,6 +15,11 @@ */ 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.List; @@ -26,6 +31,7 @@ import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -73,6 +79,33 @@ protected PreferredConstructorDiscoverer(TypeInformation type, @Nullable Pers int numberOfArgConstructors = 0; Class rawOwningType = type.getType(); + if (ReflectionUtils.isKotlinClass(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; + } + } + + KFunction primaryConstructor = KClasses + .getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(type.getType())); + Constructor javaConstructor = ReflectJvmMapping.getJavaConstructor(primaryConstructor); + if (javaConstructor != null) { + this.constructor = buildPreferredConstructor(javaConstructor, type, entity); + return; + } + } + for (Constructor candidate : rawOwningType.getDeclaredConstructors()) { PreferredConstructor preferredConstructor = buildPreferredConstructor(candidate, type, entity); diff --git a/src/main/java/org/springframework/data/util/ReflectionUtils.java b/src/main/java/org/springframework/data/util/ReflectionUtils.java index 726a487321..7e798053c0 100644 --- a/src/main/java/org/springframework/data/util/ReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/ReflectionUtils.java @@ -40,19 +40,23 @@ /** * Spring Data specific reflection utility methods and classes. - * + * * @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. - * + * * @param classname the fully qualified class name to create an instance for. * @param defaultInstance the instance to fall back to in case the given class cannot be loaded or instantiated. * @return @@ -70,7 +74,7 @@ public static T createInstanceIfPresent(String classname, T defaultInstance) /** * A {@link FieldFilter} that has a description. - * + * * @author Oliver Gierke */ public interface DescribedFieldFilter extends FieldFilter { @@ -78,7 +82,7 @@ public interface DescribedFieldFilter extends FieldFilter { /** * Returns the description of the field filter. Used in exceptions being thrown in case uniqueness shall be enforced * on the field filter. - * + * * @return */ String getDescription(); @@ -86,7 +90,7 @@ public interface DescribedFieldFilter extends FieldFilter { /** * A {@link FieldFilter} for a given annotation. - * + * * @author Oliver Gierke */ @RequiredArgsConstructor @@ -94,7 +98,7 @@ public static class AnnotationFieldFilter implements DescribedFieldFilter { private final @NonNull Class annotationType; - /* + /* * (non-Javadoc) * @see org.springframework.util.ReflectionUtils.FieldFilter#matches(java.lang.reflect.Field) */ @@ -102,7 +106,7 @@ public boolean matches(Field field) { return AnnotationUtils.getAnnotation(field, annotationType) != null; } - /* + /* * (non-Javadoc) * @see org.springframework.data.util.ReflectionUtils.DescribedFieldFilter#getDescription() */ @@ -113,7 +117,7 @@ public String getDescription() { /** * Finds the first field on the given class matching the given {@link FieldFilter}. - * + * * @param type must not be {@literal null}. * @param filter must not be {@literal null}. * @return the field matching the filter or {@literal null} in case no field could be found. @@ -136,7 +140,7 @@ public String getDescription() { /** * Finds the field matching the given {@link DescribedFieldFilter}. Will make sure there's only one field matching the * filter. - * + * * @see #findField(Class, DescribedFieldFilter, boolean) * @param type must not be {@literal null}. * @param filter must not be {@literal null}. @@ -151,7 +155,7 @@ public static Field findField(Class type, DescribedFieldFilter filter) { /** * Finds the field matching the given {@link DescribedFieldFilter}. Will make sure there's only one field matching the * filter in case {@code enforceUniqueness} is {@literal true}. - * + * * @param type must not be {@literal null}. * @param filter must not be {@literal null}. * @param enforceUniqueness whether to enforce uniqueness of the field @@ -194,7 +198,7 @@ public static Field findField(Class type, DescribedFieldFilter filter, boolea /** * Finds the field of the given name on the given type. - * + * * @param type must not be {@literal null}. * @param name must not be {@literal null} or empty. * @return @@ -213,7 +217,7 @@ public static Field findRequiredField(Class type, String name) { /** * Sets the given field on the given object to the given value. Will make sure the given field is accessible. - * + * * @param field must not be {@literal null}. * @param target must not be {@literal null}. * @param value @@ -226,7 +230,7 @@ public static void setField(Field field, Object target, @Nullable Object value) /** * Finds a constructor on the given type that matches the given constructor arguments. - * + * * @param type must not be {@literal null}. * @param constructorArguments must not be {@literal null}. * @return a {@link Constructor} that is compatible with the given arguments. @@ -243,7 +247,7 @@ public static Optional> findConstructor(Class type, Object... /** * Returns the method with the given name of the given class and parameter types. - * + * * @param type must not be {@literal null}. * @param name must not be {@literal null}. * @param parameterTypes must not be {@literal null}. @@ -269,7 +273,7 @@ public static Method findRequiredMethod(Class type, String name, Class... /** * Returns a {@link Stream} of the return and parameters types of the given {@link Method}. - * + * * @param method must not be {@literal null}. * @return * @since 2.0 @@ -286,7 +290,7 @@ public static Stream> returnTypeAndParameters(Method method) { /** * Returns the {@link Method} with the given name and parameters declared on the given type, if available. - * + * * @param type must not be {@literal null}. * @param name must not be {@literal null} or empty. * @param parameterTypes must not be {@literal null}. @@ -338,4 +342,17 @@ private static boolean argumentsMatch(Class[] parameterTypes, Object[] argume 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/kotlin/org/springframework/data/convert/ClassGeneratingEntityInstantiatorDataClassUnitTests.kt b/src/test/kotlin/org/springframework/data/convert/ClassGeneratingEntityInstantiatorDataClassUnitTests.kt new file mode 100644 index 0000000000..f7d5f5a752 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/convert/ClassGeneratingEntityInstantiatorDataClassUnitTests.kt @@ -0,0 +1,98 @@ +/* + * 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 [ClassGeneratingEntityInstantiator] creating instances using Kotlin data classes. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner::class) +@Suppress("UNCHECKED_CAST") +class ClassGeneratingEntityInstantiatorDataClassUnitTests { + + @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(Contact::class.java).constructor + + doReturn("Walter", "White").`when`(provider).getParameterValue(any()) + doReturn(constructor).whenever(entity).persistenceConstructor + doReturn(constructor.constructor.declaringClass).whenever(entity).type + + val instance: Contact = ClassGeneratingEntityInstantiator().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(ContactWithDefaulting::class.java).constructor + + 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 = ClassGeneratingEntityInstantiator().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 0000000000..610660b32c --- /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(Contact::class.java).constructor + + 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(ContactWithDefaulting::class.java).constructor + + 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 0000000000..b50d0054bb --- /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(Simple::class.java).constructor + + Assertions.assertThat(constructor.parameters.size).isEqualTo(1) + } + + @Test // DATACMNS-1126 + fun `should reject two constructors`() { + + val constructor = PreferredConstructorDiscoverer(TwoConstructors::class.java).constructor + + Assertions.assertThat(constructor.parameters.size).isEqualTo(1) + } + + @Test // DATACMNS-1126 + fun `should discover annotated constructor`() { + + val constructor = PreferredConstructorDiscoverer(AnnotatedConstructors::class.java).constructor + + Assertions.assertThat(constructor.parameters.size).isEqualTo(2) + } + + @Test // DATACMNS-1126 + fun `should discover default constructor`() { + + val constructor = PreferredConstructorDiscoverer(DefaultConstructor::class.java).constructor + + Assertions.assertThat(constructor.parameters.size).isEqualTo(1) + } + + @Test // DATACMNS-1126 + fun `should discover default annotated constructor`() { + + val constructor = PreferredConstructorDiscoverer(TwoDefaultConstructorsAnnotated::class.java).constructor + + 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