, 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 extends T, P> 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 ({@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 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 extends T, P> 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 extends Annotation> 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