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