> InstanceCreatorMetadata discover(PersistentEntity entity) {
+ public static > InstanceCreatorMetadata discover(
+ PersistentEntity entity) {
Constructor>[] declaredConstructors = entity.getType().getDeclaredConstructors();
Method[] declaredMethods = entity.getType().getDeclaredMethods();
@@ -78,6 +85,34 @@ public static > InstanceCreatorMetadata di
}
}
+ if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(entity.getType())) {
+
+ KClass> kClass = JvmClassMappingKt.getKotlinClass(entity.getType());
+ // We use box-impl as factory for classes.
+ if (kClass.isValue()) {
+
+ String propertyName = "";
+ for (KCallable> member : kClass.getMembers()) {
+ if (member instanceof KProperty>) {
+ propertyName = member.getName();
+ break;
+ }
+ }
+
+ for (Method declaredMethod : entity.getType().getDeclaredMethods()) {
+ if (declaredMethod.getName().equals("box-impl") && declaredMethod.isSynthetic()
+ && declaredMethod.getParameterCount() == 1) {
+
+ Annotation[][] parameterAnnotations = declaredMethod.getParameterAnnotations();
+ List> types = entity.getTypeInformation().getParameterTypes(declaredMethod);
+
+ return new FactoryMethod<>(declaredMethod,
+ new Parameter<>(propertyName, (TypeInformation) types.get(0), parameterAnnotations[0], entity));
+ }
+ }
+ }
+ }
+
return PreferredConstructorDiscoverer.discover(entity);
}
diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java
index 8c29fab1c4..f5570c49f6 100644
--- a/src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java
+++ b/src/main/java/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiator.java
@@ -15,23 +15,14 @@
*/
package org.springframework.data.mapping.model;
-import kotlin.reflect.KFunction;
-import kotlin.reflect.KParameter;
-import kotlin.reflect.jvm.ReflectJvmMapping;
-
import java.lang.reflect.Constructor;
import java.util.Arrays;
-import java.util.List;
-import java.util.stream.IntStream;
import org.springframework.data.mapping.InstanceCreatorMetadata;
-import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.util.KotlinReflectionUtils;
-import org.springframework.data.util.ReflectionUtils;
-import org.springframework.lang.Nullable;
/**
* Kotlin-specific extension to {@link ClassGeneratingEntityInstantiator} that adapts Kotlin constructors with
@@ -52,102 +43,21 @@ protected EntityInstantiator doCreateEntityInstantiator(PersistentEntity, ?> e
if (KotlinReflectionUtils.isSupportedKotlinClass(entity.getType())
&& creator instanceof PreferredConstructor, ?> constructor) {
- PreferredConstructor, ? extends PersistentProperty>> defaultConstructor = new DefaultingKotlinConstructorResolver(
- entity)
- .getDefaultConstructor();
+ PreferredConstructor, ? extends PersistentProperty>> kotlinJvmConstructor = KotlinInstantiationDelegate
+ .resolveKotlinJvmConstructor(constructor);
- if (defaultConstructor != null) {
+ if (kotlinJvmConstructor != null) {
- ObjectInstantiator instantiator = createObjectInstantiator(entity, defaultConstructor);
+ ObjectInstantiator instantiator = createObjectInstantiator(entity, kotlinJvmConstructor);
- return new DefaultingKotlinClassInstantiatorAdapter(instantiator, constructor);
+ return new DefaultingKotlinClassInstantiatorAdapter(instantiator, constructor,
+ kotlinJvmConstructor.getConstructor());
}
}
return super.doCreateEntityInstantiator(entity);
}
- /**
- * Resolves a {@link PreferredConstructor} to a synthetic Kotlin constructor accepting the same user-space parameters
- * suffixed by Kotlin-specifics required for defaulting and the {@code kotlin.jvm.internal.DefaultConstructorMarker}.
- *
- * @since 2.0
- * @author Mark Paluch
- */
- static class DefaultingKotlinConstructorResolver {
-
- private final @Nullable PreferredConstructor, ?> defaultConstructor;
-
- @SuppressWarnings("unchecked")
- DefaultingKotlinConstructorResolver(PersistentEntity, ?> entity) {
-
- Constructor> hit = resolveDefaultConstructor(entity);
- InstanceCreatorMetadata extends PersistentProperty>> creator = entity.getInstanceCreatorMetadata();
-
- if ((hit != null) && creator instanceof PreferredConstructor, ?> persistenceConstructor) {
- this.defaultConstructor = new PreferredConstructor<>(hit,
- persistenceConstructor.getParameters().toArray(new Parameter[0]));
- } else {
- this.defaultConstructor = null;
- }
- }
-
- @Nullable
- private static Constructor> resolveDefaultConstructor(PersistentEntity, ?> entity) {
-
- if (!(entity.getInstanceCreatorMetadata() instanceof PreferredConstructor, ?> persistenceConstructor)) {
- return null;
- }
-
- Constructor> hit = null;
- Constructor> constructor = persistenceConstructor.getConstructor();
-
- for (Constructor> candidate : entity.getType().getDeclaredConstructors()) {
-
- // use only synthetic constructors
- if (!candidate.isSynthetic()) {
- continue;
- }
-
- // candidates must contain at least two additional parameters (int, DefaultConstructorMarker).
- // Number of defaulting masks derives from the original constructor arg count
- int syntheticParameters = KotlinDefaultMask.getMaskCount(constructor.getParameterCount())
- + /* DefaultConstructorMarker */ 1;
-
- if ((constructor.getParameterCount() + syntheticParameters) != 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
@@ -169,23 +79,13 @@ private static boolean parametersMatch(java.lang.reflect.Parameter[] constructor
static class DefaultingKotlinClassInstantiatorAdapter implements EntityInstantiator {
private final ObjectInstantiator instantiator;
- private final KFunction> constructor;
- private final List kParameters;
- private final Constructor> synthetic;
+ private final KotlinInstantiationDelegate delegate;
- DefaultingKotlinClassInstantiatorAdapter(ObjectInstantiator instantiator, PreferredConstructor, ?> constructor) {
-
- KFunction> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(constructor.getConstructor());
-
- if (kotlinConstructor == null) {
- throw new IllegalArgumentException(
- "No corresponding Kotlin constructor found for " + constructor.getConstructor());
- }
+ DefaultingKotlinClassInstantiatorAdapter(ObjectInstantiator instantiator,
+ PreferredConstructor, ?> defaultConstructor, Constructor> constructorToInvoke) {
this.instantiator = instantiator;
- this.constructor = kotlinConstructor;
- this.kParameters = kotlinConstructor.getParameters();
- this.synthetic = constructor.getConstructor();
+ this.delegate = new KotlinInstantiationDelegate(defaultConstructor, constructorToInvoke);
}
@Override
@@ -193,7 +93,8 @@ static class DefaultingKotlinClassInstantiatorAdapter implements EntityInstantia
public , P extends PersistentProperty> T createInstance(E entity,
ParameterValueProvider
provider) {
- Object[] params = extractInvocationArguments(entity.getInstanceCreatorMetadata(), provider);
+ Object[] params = allocateArguments(delegate.getRequiredParameterCount());
+ delegate.extractInvocationArguments(params, entity.getInstanceCreatorMetadata(), provider);
try {
return (T) instantiator.newInstance(params);
@@ -202,52 +103,5 @@ public , P extends PersistentPrope
}
}
- private , T> Object[] extractInvocationArguments(
- @Nullable InstanceCreatorMetadata
entityCreator, ParameterValueProvider
provider) {
-
- if (entityCreator == null) {
- throw new IllegalArgumentException("EntityCreator must not be null");
- }
-
- Object[] params = allocateArguments(synthetic.getParameterCount()
- + KotlinDefaultMask.getMaskCount(synthetic.getParameterCount()) + /* DefaultConstructorMarker */1);
- int userParameterCount = kParameters.size();
-
- List> parameters = entityCreator.getParameters();
-
- // Prepare user-space arguments
- for (int i = 0; i < userParameterCount; i++) {
-
- Parameter parameter = parameters.get(i);
- params[i] = provider.getParameterValue(parameter);
- }
-
- KotlinDefaultMask defaultMask = KotlinDefaultMask.from(constructor, it -> {
-
- int index = kParameters.indexOf(it);
-
- Parameter parameter = parameters.get(index);
- Class type = parameter.getType().getType();
-
- if (it.isOptional() && (params[index] == null)) {
- if (type.isPrimitive()) {
-
- // apply primitive defaulting to prevent NPE on primitive downcast
- params[index] = ReflectionUtils.getPrimitiveDefault(type);
- }
- return false;
- }
-
- return true;
- });
-
- int[] defaulting = defaultMask.getDefaulting();
- // append nullability masks to creation arguments
- for (int i = 0; i < defaulting.length; i++) {
- params[userParameterCount + i] = defaulting[i];
- }
-
- return params;
- }
}
}
diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java b/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java
index d973ded20d..0794dbdadf 100644
--- a/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java
+++ b/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java
@@ -31,6 +31,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.springframework.core.ResolvableType;
@@ -67,10 +68,9 @@ private KotlinCopyMethod(Method publicCopyMethod, Method syntheticCopyMethod) {
}
/**
- * Attempt to lookup the Kotlin {@code copy} method. Lookup happens in two stages: Find the synthetic copy method and
+ * Attempt to look up the Kotlin {@code copy} method. Lookup happens in two stages: Find the synthetic copy method and
* then attempt to resolve its public variant.
*
- * @param property the property that must be included in the copy method.
* @param type the class.
* @return {@link Optional} {@link KotlinCopyMethod}.
*/
@@ -171,13 +171,27 @@ private static Optional findPublicCopyMethod(Method defaultKotlinMethod)
return Optional.empty();
}
+ boolean usesValueClasses = KotlinValueUtils.hasValueClassProperty(type);
List constructorArguments = getComponentArguments(primaryConstructor);
+ Predicate isCopyMethod;
- return Arrays.stream(type.getDeclaredMethods()).filter(it -> it.getName().equals("copy") //
- && !it.isSynthetic() //
- && !Modifier.isStatic(it.getModifiers()) //
- && it.getReturnType().equals(type) //
- && it.getParameterCount() == constructorArguments.size()) //
+ if (usesValueClasses) {
+ String methodName = defaultKotlinMethod.getName();
+ Assert.isTrue(methodName.contains("$"), () -> "Cannot find $ marker in method name " + defaultKotlinMethod);
+
+ String methodNameWithHash = methodName.substring(0, methodName.indexOf("$"));
+ isCopyMethod = it -> it.equals(methodNameWithHash);
+ } else {
+ isCopyMethod = it -> it.equals("copy");
+ }
+
+ return Arrays.stream(type.getDeclaredMethods()).filter(it -> {
+ return isCopyMethod.test(it.getName()) //
+ && !it.isSynthetic() //
+ && !Modifier.isStatic(it.getModifiers()) //
+ && it.getReturnType().equals(type) //
+ && it.getParameterCount() == constructorArguments.size();
+ }) //
.filter(it -> {
KFunction> kotlinFunction = ReflectJvmMapping.getKotlinFunction(it);
@@ -228,13 +242,17 @@ private static Optional findSyntheticCopyMethod(Class> type) {
return Optional.empty();
}
+ boolean usesValueClasses = KotlinValueUtils.hasValueClassProperty(type);
+
+ Predicate isCopyMethod = usesValueClasses ? (it -> it.startsWith("copy-") && it.endsWith("$default"))
+ : (it -> it.equals("copy$default"));
+
return Arrays.stream(type.getDeclaredMethods()) //
- .filter(it -> it.getName().equals("copy$default") //
+ .filter(it -> isCopyMethod.test(it.getName()) //
&& Modifier.isStatic(it.getModifiers()) //
&& it.getReturnType().equals(type))
.filter(Method::isSynthetic) //
- .filter(it -> matchesPrimaryConstructor(it.getParameterTypes(), primaryConstructor))
- .findFirst();
+ .filter(it -> matchesPrimaryConstructor(it.getParameterTypes(), primaryConstructor)).findFirst();
}
/**
@@ -244,7 +262,7 @@ private static boolean matchesPrimaryConstructor(Class>[] parameterTypes, KFun
List constructorArguments = getComponentArguments(primaryConstructor);
- int defaultingArgs = KotlinDefaultMask.from(primaryConstructor, kParameter -> false).getDefaulting().length;
+ int defaultingArgs = KotlinDefaultMask.forCopy(primaryConstructor, kParameter -> false).getDefaulting().length;
if (parameterTypes.length != 1 /* $this */ + constructorArguments.size() + defaultingArgs + 1 /* object marker */) {
return false;
@@ -259,6 +277,12 @@ private static boolean matchesPrimaryConstructor(Class>[] parameterTypes, KFun
KParameter kParameter = constructorArguments.get(i);
+ if (KotlinValueUtils.isValueClass(kParameter.getType())) {
+ // sigh. This can require deep unwrapping because the public vs. the synthetic copy methods use different
+ // parameter types.
+ continue;
+ }
+
if (!isAssignableFrom(parameterTypes[i + 1], kParameter.getType())) {
return false;
}
@@ -297,7 +321,7 @@ static class KotlinCopyByProperty {
this.parameterPosition = findIndex(copyFunction, property.getName());
this.parameterCount = copyFunction.getParameters().size();
- this.defaultMask = KotlinDefaultMask.from(copyFunction, it -> property.getName().equals(it.getName()));
+ this.defaultMask = KotlinDefaultMask.forCopy(copyFunction, it -> property.getName().equals(it.getName()));
}
static int findIndex(KFunction> function, String parameterName) {
diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java b/src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java
index d7aeadf5a3..41a6e591f9 100644
--- a/src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java
+++ b/src/main/java/org/springframework/data/mapping/model/KotlinDefaultMask.java
@@ -54,12 +54,23 @@ public void forEach(IntConsumer maskCallback) {
* Return the number of defaulting masks required to represent the number of {@code arguments}.
*
* @param arguments number of method arguments.
- * @return the number of defaulting masks required.
+ * @return the number of defaulting masks required. Returns at least one to be used with {@code copy} methods.
*/
public static int getMaskCount(int arguments) {
return ((arguments - 1) / Integer.SIZE) + 1;
}
+ /**
+ * Return the number of defaulting masks required to represent the number of {@code optionalParameterCount}.
+ * In contrast to {@link #getMaskCount(int)}, this method can return zero if there are no optional parameters available.
+ *
+ * @param optionalParameterCount number of method arguments.
+ * @return the number of defaulting masks required. Returns zero if no optional parameters are available.
+ */
+ static int getExactMaskCount(int optionalParameterCount) {
+ return optionalParameterCount == 0 ? 0 : getMaskCount(optionalParameterCount);
+ }
+
/**
* Creates defaulting mask(s) used to invoke Kotlin {@literal default} methods that conditionally apply parameter
* values.
@@ -69,10 +80,47 @@ public static int getMaskCount(int arguments) {
* @return {@link KotlinDefaultMask}.
*/
public static KotlinDefaultMask from(KFunction> function, Predicate isPresent) {
+ return forCopy(function, isPresent);
+ }
+
+ /**
+ * Creates defaulting mask(s) used to invoke Kotlin {@literal copy} methods that conditionally apply parameter values.
+ *
+ * @param function the {@link KFunction} that should be invoked.
+ * @param isPresent {@link Predicate} for the presence/absence of parameters.
+ * @return {@link KotlinDefaultMask}.
+ */
+ static KotlinDefaultMask forCopy(KFunction> function, Predicate isPresent) {
+ return from(function, isPresent, true);
+ }
+
+ /**
+ * Creates defaulting mask(s) used to invoke Kotlin constructors where a defaulting mask isn't required unless there's
+ * one nullable argument.
+ *
+ * @param function the {@link KFunction} that should be invoked.
+ * @param isPresent {@link Predicate} for the presence/absence of parameters.
+ * @return {@link KotlinDefaultMask}.
+ */
+ static KotlinDefaultMask forConstructor(KFunction> function, Predicate isPresent) {
+ return from(function, isPresent, false);
+ }
+
+ /**
+ * Creates defaulting mask(s) used to invoke Kotlin {@literal default} methods that conditionally apply parameter
+ * values.
+ *
+ * @param function the {@link KFunction} that should be invoked.
+ * @param isPresent {@link Predicate} for the presence/absence of parameters.
+ * @return {@link KotlinDefaultMask}.
+ */
+ private static KotlinDefaultMask from(KFunction> function, Predicate isPresent,
+ boolean requiresAtLeastOneMask) {
List masks = new ArrayList<>();
int index = 0;
int mask = 0;
+ boolean hasSeenParameter = false;
List parameters = function.getParameters();
@@ -83,8 +131,12 @@ public static KotlinDefaultMask from(KFunction> function, Predicate function, Predicate i).toArray());
}
diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinInstantiationDelegate.java b/src/main/java/org/springframework/data/mapping/model/KotlinInstantiationDelegate.java
new file mode 100644
index 0000000000..c3b1bc09ac
--- /dev/null
+++ b/src/main/java/org/springframework/data/mapping/model/KotlinInstantiationDelegate.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2023 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mapping.model;
+
+import kotlin.reflect.KFunction;
+import kotlin.reflect.KParameter;
+import kotlin.reflect.jvm.ReflectJvmMapping;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+
+import org.springframework.data.mapping.InstanceCreatorMetadata;
+import org.springframework.data.mapping.Parameter;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.PreferredConstructor;
+import org.springframework.data.mapping.model.KotlinValueUtils.ValueBoxing;
+import org.springframework.data.util.ReflectionUtils;
+import org.springframework.lang.Nullable;
+
+/**
+ * Delegate to allocate instantiation arguments and to resolve the actual constructor to call for inline/value class
+ * instantiation. This class captures all Kotlin-specific quirks, from default constructor resolution up to mangled
+ * inline-type handling so instantiator-components can delegate to this class for constructor translation and
+ * constructor argument extraction.
+ *
+ * @author Mark Paluch
+ * @since 3.1
+ */
+class KotlinInstantiationDelegate {
+
+ private final KFunction> constructor;
+ private final List kParameters;
+ private final List> wrappers = new ArrayList<>();
+ private final Constructor> constructorToInvoke;
+ private final boolean hasDefaultConstructorMarker;
+
+ public KotlinInstantiationDelegate(PreferredConstructor, ?> preferredConstructor,
+ Constructor> constructorToInvoke) {
+
+ KFunction> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(preferredConstructor.getConstructor());
+
+ if (kotlinConstructor == null) {
+ throw new IllegalArgumentException(
+ "No corresponding Kotlin constructor found for " + preferredConstructor.getConstructor());
+ }
+
+ this.constructor = kotlinConstructor;
+ this.kParameters = kotlinConstructor.getParameters();
+ this.constructorToInvoke = constructorToInvoke;
+ this.hasDefaultConstructorMarker = hasDefaultConstructorMarker(constructorToInvoke.getParameters());
+
+ for (KParameter kParameter : kParameters) {
+
+ ValueBoxing valueBoxing = KotlinValueUtils.getConstructorValueHierarchy(kParameter);
+ wrappers.add(valueBoxing::wrap);
+ }
+ }
+
+ static boolean hasDefaultConstructorMarker(java.lang.reflect.Parameter[] parameters) {
+
+ return parameters.length > 0
+ && parameters[parameters.length - 1].getType().getName().equals("kotlin.jvm.internal.DefaultConstructorMarker");
+ }
+
+ /**
+ * @return number of constructor arguments.
+ */
+ public int getRequiredParameterCount() {
+
+ return hasDefaultConstructorMarker ? constructorToInvoke.getParameterCount()
+ : (constructorToInvoke.getParameterCount()
+ + KotlinDefaultMask.getMaskCount(constructorToInvoke.getParameterCount())
+ + /* DefaultConstructorMarker */1);
+ }
+
+ /**
+ * Extract the actual construction arguments for a direct constructor call.
+ *
+ * @param params
+ * @param entityCreator
+ * @param provider
+ * @return
+ * @param
+ */
+ public
> Object[] extractInvocationArguments(Object[] params,
+ @Nullable InstanceCreatorMetadata
entityCreator, ParameterValueProvider
provider) {
+
+ if (entityCreator == null) {
+ throw new IllegalArgumentException("EntityCreator must not be null");
+ }
+
+ int userParameterCount = kParameters.size();
+
+ List> parameters = entityCreator.getParameters();
+
+ // Prepare user-space arguments
+ for (int i = 0; i < userParameterCount; i++) {
+
+ Parameter parameter = parameters.get(i);
+ params[i] = provider.getParameterValue(parameter);
+ }
+
+ KotlinDefaultMask defaultMask = KotlinDefaultMask.forConstructor(constructor, it -> {
+
+ int index = kParameters.indexOf(it);
+
+ Parameter parameter = parameters.get(index);
+ Class type = parameter.getType().getType();
+
+ if (it.isOptional() && (params[index] == null)) {
+ if (type.isPrimitive()) {
+
+ // apply primitive defaulting to prevent NPE on primitive downcast
+ params[index] = ReflectionUtils.getPrimitiveDefault(type);
+ }
+ return false;
+ }
+
+ return true;
+ });
+
+ // late rewrapping to indicate potential absence of parameters for defaulting
+ for (int i = 0; i < userParameterCount; i++) {
+ params[i] = wrappers.get(i).apply(params[i]);
+ }
+
+ int[] defaulting = defaultMask.getDefaulting();
+ // append nullability masks to creation arguments
+ for (int i = 0; i < defaulting.length; i++) {
+ params[userParameterCount + i] = defaulting[i];
+ }
+
+ return params;
+ }
+
+ /**
+ * 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
+ */
+
+ @SuppressWarnings("unchecked")
+ @Nullable
+ public static PreferredConstructor, ?> resolveKotlinJvmConstructor(
+ PreferredConstructor, ?> preferredConstructor) {
+
+ Constructor> hit = doResolveKotlinConstructor(preferredConstructor.getConstructor());
+
+ if (hit == preferredConstructor.getConstructor()) {
+ return preferredConstructor;
+ }
+
+ if (hit != null) {
+ return new PreferredConstructor<>(hit, preferredConstructor.getParameters().toArray(new Parameter[0]));
+ }
+
+ return null;
+ }
+
+ @Nullable
+ private static Constructor> doResolveKotlinConstructor(Constructor> detectedConstructor) {
+
+ Class> entityType = detectedConstructor.getDeclaringClass();
+ Constructor> hit = null;
+ KFunction> kotlinFunction = ReflectJvmMapping.getKotlinFunction(detectedConstructor);
+
+ for (Constructor> candidate : entityType.getDeclaredConstructors()) {
+
+ // use only synthetic constructors
+ if (!candidate.isSynthetic()) {
+ continue;
+ }
+
+ java.lang.reflect.Parameter[] detectedConstructorParameters = detectedConstructor.getParameters();
+ java.lang.reflect.Parameter[] candidateParameters = candidate.getParameters();
+
+ if (!KotlinInstantiationDelegate.hasDefaultConstructorMarker(detectedConstructorParameters)) {
+
+ // candidates must contain at least two additional parameters (int, DefaultConstructorMarker).
+ // Number of defaulting masks derives from the original constructor arg count
+ int syntheticParameters = KotlinDefaultMask.getMaskCount(detectedConstructor.getParameterCount())
+ + /* DefaultConstructorMarker */ 1;
+
+ if ((detectedConstructor.getParameterCount() + syntheticParameters) != candidate.getParameterCount()) {
+ continue;
+ }
+ } else {
+
+ int optionalParameterCount = (int) kotlinFunction.getParameters().stream().filter(it -> it.isOptional())
+ .count();
+ int syntheticParameters = KotlinDefaultMask.getExactMaskCount(optionalParameterCount);
+
+ if ((detectedConstructor.getParameterCount() + syntheticParameters) != candidate.getParameterCount()) {
+ continue;
+ }
+ }
+
+ if (!KotlinInstantiationDelegate.hasDefaultConstructorMarker(candidateParameters)) {
+ continue;
+ }
+
+ int userParameterCount = kotlinFunction != null ? kotlinFunction.getParameters().size()
+ : detectedConstructor.getParameterCount();
+ if (parametersMatch(detectedConstructorParameters, candidateParameters, userParameterCount)) {
+ hit = candidate;
+ }
+ }
+
+ return hit;
+ }
+
+ private static boolean parametersMatch(java.lang.reflect.Parameter[] constructorParameters,
+ java.lang.reflect.Parameter[] candidateParameters, int userParameterCount) {
+
+ return IntStream.range(0, userParameterCount)
+ .allMatch(i -> parametersMatch(constructorParameters[i], candidateParameters[i]));
+ }
+
+ static boolean parametersMatch(java.lang.reflect.Parameter constructorParameter,
+ java.lang.reflect.Parameter candidateParameter) {
+
+ if (constructorParameter.getType().equals(candidateParameter.getType())) {
+ return true;
+ }
+
+ // candidate can be also a wrapper
+ Class> componentOrWrapperType = KotlinValueUtils.getConstructorValueHierarchy(candidateParameter.getType())
+ .getActualType();
+
+ return constructorParameter.getType().equals(componentOrWrapperType);
+ }
+}
diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java b/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java
new file mode 100644
index 0000000000..02ea07b779
--- /dev/null
+++ b/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright 2023 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mapping.model;
+
+import kotlin.jvm.JvmClassMappingKt;
+import kotlin.jvm.internal.Reflection;
+import kotlin.reflect.KCallable;
+import kotlin.reflect.KClass;
+import kotlin.reflect.KFunction;
+import kotlin.reflect.KParameter;
+import kotlin.reflect.KProperty;
+import kotlin.reflect.KType;
+import kotlin.reflect.KTypeParameter;
+import kotlin.reflect.jvm.ReflectJvmMapping;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.core.KotlinDetector;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Utilities for Kotlin Value class support.
+ *
+ * @author Mark Paluch
+ * @since 3.2
+ */
+class KotlinValueUtils {
+
+ /**
+ * Returns whether the given {@link KType} is a {@link KClass#isValue() value} class.
+ *
+ * @param type the kotlin type to inspect.
+ * @return {@code true} the type is a value class.
+ */
+ public static boolean isValueClass(KType type) {
+ return type.getClassifier()instanceof KClass> kc && kc.isValue();
+ }
+
+ /**
+ * Returns whether the given class makes uses Kotlin {@link KClass#isValue() value} classes.
+ *
+ * @param type the kotlin type to inspect.
+ * @return {@code true} when at least one property uses Kotlin value classes.
+ */
+ public static boolean hasValueClassProperty(Class> type) {
+
+ if (!KotlinDetector.isKotlinType(type)) {
+ return false;
+ }
+
+ KClass> kotlinClass = JvmClassMappingKt.getKotlinClass(type);
+
+ for (KCallable> member : kotlinClass.getMembers()) {
+ if (member instanceof KProperty> kp && isValueClass(kp.getReturnType())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Creates a value hierarchy across value types from a given {@link KParameter} for COPY method usage.
+ *
+ * @param parameter the parameter that references the value class hierarchy.
+ * @return
+ */
+ public static ValueBoxing getCopyValueHierarchy(KParameter parameter) {
+ return new ValueBoxing(BoxingRules.COPY, parameter);
+ }
+
+ /**
+ * Creates a value hierarchy across value types from a given {@link KParameter} for constructor usage.
+ *
+ * @param parameter the parameter that references the value class hierarchy.
+ * @return the {@link ValueBoxing} type hierarchy.
+ */
+ public static ValueBoxing getConstructorValueHierarchy(KParameter parameter) {
+ return new ValueBoxing(BoxingRules.CONSTRUCTOR, parameter);
+ }
+
+ /**
+ * Creates a value hierarchy across value types from a given {@link KParameter} for constructor usage.
+ *
+ * @param cls the entrypoint of the type hierarchy.
+ * @return the {@link ValueBoxing} type hierarchy.
+ */
+ public static ValueBoxing getConstructorValueHierarchy(Class> cls) {
+
+ KClass> kotlinClass = JvmClassMappingKt.getKotlinClass(cls);
+ return new ValueBoxing(BoxingRules.CONSTRUCTOR, Reflection.typeOf(kotlinClass), kotlinClass, false);
+ }
+
+ /**
+ * Boxing rules for value class wrappers.
+ */
+ enum BoxingRules {
+
+ /**
+ * When used in the constructor. Constructor boxing depends on nullability of the declared property, whether the
+ * component uses defaulting, nullability of the value component and whether the component is a primitive.
+ */
+ CONSTRUCTOR {
+ @Override
+ public boolean shouldApplyBoxing(KType type, boolean optional, KParameter component) {
+
+ Type javaType = ReflectJvmMapping.getJavaType(component.getType());
+ boolean isPrimitive = javaType instanceof Class> c && c.isPrimitive();
+
+ if (type.isMarkedNullable() || optional) {
+ return (isPrimitive && type.isMarkedNullable()) || component.getType().isMarkedNullable();
+ }
+
+ return false;
+ }
+ },
+
+ /**
+ * When used in a copy method. Copy method boxing depends on nullability of the declared property, nullability of
+ * the value component and whether the component is a primitive.
+ */
+ COPY {
+ @Override
+ public boolean shouldApplyBoxing(KType type, boolean optional, KParameter component) {
+
+ KType copyType = expandUnderlyingType(type);
+
+ if (copyType.getClassifier()instanceof KClass> kc && kc.isValue() || copyType.isMarkedNullable()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static KType expandUnderlyingType(KType kotlinType) {
+
+ if (!(kotlinType.getClassifier()instanceof KClass> kc) || !kc.isValue()) {
+ return kotlinType;
+ }
+
+ List> properties = getProperties(kc);
+ if (properties.isEmpty()) {
+ return kotlinType;
+ }
+
+ KType underlyingType = properties.get(0).getReturnType();
+ KType componentType = ValueBoxing.resolveType(underlyingType);
+ KType expandedUnderlyingType = expandUnderlyingType(componentType);
+
+ if (!kotlinType.isMarkedNullable()) {
+ return expandedUnderlyingType;
+ }
+
+ if (expandedUnderlyingType.isMarkedNullable()) {
+ return kotlinType;
+ }
+
+ Type javaType = ReflectJvmMapping.getJavaType(expandedUnderlyingType);
+ boolean isPrimitive = javaType instanceof Class> c && c.isPrimitive();
+
+ if (isPrimitive) {
+ return kotlinType;
+ }
+
+ return expandedUnderlyingType;
+ }
+
+ static List> getProperties(KClass> kClass) {
+
+ if (kClass.isValue()) {
+
+ for (KCallable> member : kClass.getMembers()) {
+ if (member instanceof KProperty> kp) {
+ return Collections.singletonList(kp);
+ }
+ }
+ }
+
+ List> properties = new ArrayList<>();
+ for (KCallable> member : kClass.getMembers()) {
+ if (member instanceof KProperty> kp) {
+ properties.add(kp);
+ }
+ }
+
+ return properties;
+ }
+ };
+
+ public abstract boolean shouldApplyBoxing(KType type, boolean optional, KParameter component);
+
+ }
+
+ /**
+ * Utility to represent Kotlin value class boxing.
+ */
+ static class ValueBoxing {
+
+ private final KClass> kClass;
+
+ private final KFunction> wrapperConstructor;
+
+ private final boolean applyBoxing;
+
+ private final @Nullable ValueBoxing next;
+
+ /**
+ * Creates a new {@link ValueBoxing} for a {@link KParameter}.
+ *
+ * @param rules boxing rules to apply.
+ * @param parameter the copy or constructor parameter.
+ */
+ @SuppressWarnings("ConstantConditions")
+ private ValueBoxing(BoxingRules rules, KParameter parameter) {
+ this(rules, parameter.getType(), (KClass>) parameter.getType().getClassifier(), parameter.isOptional());
+ }
+
+ private ValueBoxing(BoxingRules rules, KType type, KClass> kClass, boolean optional) {
+
+ KFunction> wrapperConstructor = null;
+ ValueBoxing next = null;
+ boolean applyBoxing;
+
+ if (kClass.isValue()) {
+
+ wrapperConstructor = kClass.getConstructors().iterator().next();
+ KParameter nested = wrapperConstructor.getParameters().get(0);
+ KType nestedType = nested.getType();
+
+ applyBoxing = rules.shouldApplyBoxing(type, optional, nested);
+
+ KClass> nestedClass;
+
+ // bound flattening
+ if (nestedType.getClassifier()instanceof KTypeParameter ktp) {
+ nestedClass = getUpperBound(ktp);
+ } else {
+ nestedClass = (KClass>) nestedType.getClassifier();
+ }
+
+ Assert.notNull(nestedClass, () -> String.format("Cannot resolve nested class from type %s", nestedType));
+
+ next = new ValueBoxing(rules, nestedType, nestedClass, nested.isOptional());
+ } else {
+ applyBoxing = false;
+ }
+
+ this.kClass = kClass;
+ this.wrapperConstructor = wrapperConstructor;
+ this.next = next;
+ this.applyBoxing = applyBoxing;
+ }
+
+ private static KClass> getUpperBound(KTypeParameter typeParameter) {
+
+ for (KType upperBound : typeParameter.getUpperBounds()) {
+
+ if (upperBound.getClassifier()instanceof KClass> kc) {
+ return kc;
+ }
+ }
+
+ throw new IllegalArgumentException("No upper bounds found");
+ }
+
+ static KType resolveType(KType type) {
+
+ if (type.getClassifier()instanceof KTypeParameter ktp) {
+
+ for (KType upperBound : ktp.getUpperBounds()) {
+
+ if (upperBound.getClassifier()instanceof KClass> kc) {
+ return upperBound;
+ }
+ }
+ }
+
+ return type;
+ }
+
+ /**
+ * @return the expanded component type that is used as value.
+ */
+ public Class> getActualType() {
+
+ if (isValueClass() && hasNext()) {
+ return getNext().getActualType();
+ }
+
+ return JvmClassMappingKt.getJavaClass(kClass);
+ }
+
+ /**
+ * @return the component or wrapper type to be used.
+ */
+ public Class> getParameterType() {
+
+ if (hasNext() && getNext().appliesBoxing()) {
+ return next.getParameterType();
+ }
+
+ return JvmClassMappingKt.getJavaClass(kClass);
+ }
+
+ /**
+ * @return {@code true} if the value hierarchy applies boxing.
+ */
+ public boolean appliesBoxing() {
+ return applyBoxing;
+ }
+
+ public boolean isValueClass() {
+ return kClass.isValue();
+ }
+
+ /**
+ * @return whether there is another item in the value hierarchy.
+ */
+ public boolean hasNext() {
+ return next != null;
+ }
+
+ /**
+ * Returns the next {@link ValueBoxing} or throws {@link IllegalStateException} if there is no next. Make sure to
+ * check {@link #hasNext()} prior to calling this method.
+ *
+ * @return the next {@link ValueBoxing}.
+ * @throws IllegalStateException if there is no next item.
+ */
+ public ValueBoxing getNext() {
+
+ if (next == null) {
+ throw new IllegalStateException("No next ValueBoxing available");
+ }
+
+ return next;
+ }
+
+ /**
+ * Apply wrapping into the boxing wrapper type if applicable.
+ *
+ * @param o
+ * @return
+ */
+ @Nullable
+ public Object wrap(@Nullable Object o) {
+
+ if (applyBoxing) {
+ return o == null || kClass.isInstance(o) ? o : wrapperConstructor.call(next.wrap(o));
+ }
+
+ if (hasNext()) {
+ return next.wrap(o);
+ }
+
+ return o;
+ }
+
+ @Override
+ public String toString() {
+
+ StringBuilder sb = new StringBuilder();
+
+ ValueBoxing hierarchy = this;
+ while (hierarchy != null) {
+
+ if (sb.length() != 0) {
+ sb.append(" -> ");
+ }
+
+ sb.append(hierarchy.kClass.getSimpleName());
+ hierarchy = hierarchy.next;
+ }
+
+ return sb.toString();
+ }
+
+ }
+
+}
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 73e99d225b..2d1e321506 100644
--- a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java
+++ b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java
@@ -26,6 +26,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Optional;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
@@ -179,6 +180,15 @@ > PreferredConstructor discover(TypeInf
Class> rawOwningType = type.getType();
+ // Kotlin can rewrite annotated constructors into synthetic ones so we need to inspect that first.
+ Optional> first = Arrays.stream(rawOwningType.getDeclaredConstructors()) //
+ .filter(it -> it.isSynthetic() && AnnotationUtils.findAnnotation(it, PersistenceCreator.class) != null)
+ .map(it -> buildPreferredConstructor(it, type, entity)).findFirst();
+
+ if(first.isPresent()){
+ return first.get();
+ }
+
return Arrays.stream(rawOwningType.getDeclaredConstructors()) //
.filter(it -> !it.isSynthetic()) // Synthetic constructors should not be considered
// Explicitly defined creator trumps all
diff --git a/src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java b/src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java
index 212145b3c4..ba841daf59 100644
--- a/src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java
+++ b/src/main/java/org/springframework/data/mapping/model/ReflectionEntityInstantiator.java
@@ -16,18 +16,22 @@
package org.springframework.data.mapping.model;
import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
+import org.springframework.core.KotlinDetector;
import org.springframework.data.mapping.FactoryMethod;
import org.springframework.data.mapping.InstanceCreatorMetadata;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PreferredConstructor;
+import org.springframework.data.util.KotlinReflectionUtils;
import org.springframework.util.ReflectionUtils;
/**
@@ -52,6 +56,19 @@ public , P extends PersistentPrope
if (creator == null) {
return instantiateClass(entity);
}
+
+ // workaround as classes using value classes cannot be instantiated through BeanUtils.
+ if (KotlinDetector.isKotlinReflectPresent() && KotlinReflectionUtils.isSupportedKotlinClass(entity.getType())
+ && creator instanceof PreferredConstructor, ?> constructor) {
+
+ PreferredConstructor, ? extends PersistentProperty>> kotlinJvmConstructor = KotlinInstantiationDelegate
+ .resolveKotlinJvmConstructor(constructor);
+
+ if (kotlinJvmConstructor != null) {
+ return instantiateKotlinClass(entity, provider, constructor, kotlinJvmConstructor);
+ }
+ }
+
int parameterCount = creator.getParameterCount();
Object[] params = parameterCount == 0 ? EMPTY_ARGS : new Object[parameterCount];
@@ -81,6 +98,29 @@ public , P extends PersistentPrope
}
}
+ @SuppressWarnings("unchecked")
+ private static , P extends PersistentProperty> T instantiateKotlinClass(
+ E entity, ParameterValueProvider
provider, PreferredConstructor, ?> preferredConstructor,
+ PreferredConstructor, ? extends PersistentProperty>> kotlinJvmConstructor) {
+
+ Constructor> ctor = kotlinJvmConstructor.getConstructor();
+ KotlinInstantiationDelegate delegate = new KotlinInstantiationDelegate(preferredConstructor, ctor);
+ Object[] params = new Object[delegate.getRequiredParameterCount()];
+ delegate.extractInvocationArguments(params, entity.getInstanceCreatorMetadata(), provider);
+
+ try {
+ return (T) ctor.newInstance(params);
+ } catch (InstantiationException ex) {
+ throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex);
+ } catch (IllegalAccessException ex) {
+ throw new BeanInstantiationException(ctor, "Is the preferredConstructor accessible?", ex);
+ } catch (IllegalArgumentException ex) {
+ throw new BeanInstantiationException(ctor, "Illegal arguments for preferredConstructor", ex);
+ } catch (InvocationTargetException ex) {
+ throw new BeanInstantiationException(ctor, "Constructor threw exception", ex.getTargetException());
+ }
+ }
+
@SuppressWarnings("unchecked")
private , P extends PersistentProperty> T instantiateClass(
E entity) {
diff --git a/src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java b/src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java
new file mode 100644
index 0000000000..8fc946944a
--- /dev/null
+++ b/src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2023 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
+ *
+ * https://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.util;
+
+import kotlin.jvm.JvmClassMappingKt;
+import kotlin.reflect.KCallable;
+import kotlin.reflect.KClass;
+import kotlin.reflect.KMutableProperty;
+import kotlin.reflect.KProperty;
+import kotlin.reflect.jvm.ReflectJvmMapping;
+
+import java.beans.BeanDescriptor;
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+import java.beans.SimpleBeanInfo;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.beans.BeanInfoFactory;
+import org.springframework.core.KotlinDetector;
+import org.springframework.core.Ordered;
+
+/**
+ * {@link BeanInfoFactory} specific to Kotlin types using Kotlin reflection to determine bean properties.
+ *
+ * @author Mark Paluch
+ * @since 3.2
+ * @see JvmClassMappingKt
+ * @see ReflectJvmMapping
+ */
+public class KotlinBeanInfoFactory implements BeanInfoFactory, Ordered {
+
+ @Override
+ public BeanInfo getBeanInfo(Class> beanClass) throws IntrospectionException {
+
+ if (!KotlinDetector.isKotlinReflectPresent() || !KotlinDetector.isKotlinType(beanClass)) {
+ return null;
+ }
+
+ KClass> kotlinClass = JvmClassMappingKt.getKotlinClass(beanClass);
+ List pds = new ArrayList<>();
+
+ for (KCallable> member : kotlinClass.getMembers()) {
+
+ if (member instanceof KProperty> property) {
+
+ Method getter = ReflectJvmMapping.getJavaGetter(property);
+ Method setter = property instanceof KMutableProperty> kmp ? ReflectJvmMapping.getJavaSetter(kmp) : null;
+
+ pds.add(new PropertyDescriptor(property.getName(), getter, setter));
+ }
+ }
+ return new SimpleBeanInfo() {
+ @Override
+ public BeanDescriptor getBeanDescriptor() {
+ return new BeanDescriptor(beanClass);
+ }
+
+ @Override
+ public PropertyDescriptor[] getPropertyDescriptors() {
+ return pds.toArray(new PropertyDescriptor[0]);
+ }
+ };
+ }
+
+ @Override
+ public int getOrder() {
+ return LOWEST_PRECEDENCE - 10; // leave some space for customizations.
+ }
+
+}
diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories
index fc6274377f..506aaa02df 100644
--- a/src/main/resources/META-INF/spring.factories
+++ b/src/main/resources/META-INF/spring.factories
@@ -1,3 +1,4 @@
org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SpringDataJacksonConfiguration
org.springframework.data.util.CustomCollectionRegistrar=org.springframework.data.util.CustomCollections.VavrCollections, \
org.springframework.data.util.CustomCollections.EclipseCollections
+org.springframework.beans.BeanInfoFactory=org.springframework.data.util.KotlinBeanInfoFactory
diff --git a/src/test/java/org/springframework/data/mapping/model/InstanceCreatorMetadataDiscovererUnitTests.java b/src/test/java/org/springframework/data/mapping/model/InstanceCreatorMetadataDiscovererUnitTests.java
new file mode 100644
index 0000000000..b9bd483fef
--- /dev/null
+++ b/src/test/java/org/springframework/data/mapping/model/InstanceCreatorMetadataDiscovererUnitTests.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 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
+ *
+ * https://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 static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.data.mapping.InstanceCreatorMetadata;
+import org.springframework.data.mapping.model.AbstractPersistentPropertyUnitTests.SamplePersistentProperty;
+import org.springframework.data.util.TypeInformation;
+
+/**
+ * Unit tests for {@link InstanceCreatorMetadata}.
+ *
+ * @author Mark Paluch
+ */
+class InstanceCreatorMetadataDiscovererUnitTests {
+
+ @Test
+ void shouldDiscoverConstructorForKotlinValueType() {
+
+ InstanceCreatorMetadata metadata = InstanceCreatorMetadataDiscoverer
+ .discover(new BasicPersistentEntity(
+ (TypeInformation) TypeInformation.of(MyNullableValueClass.class)));
+
+ assertThat(metadata).isNotNull();
+ assertThat(metadata.hasParameters()).isTrue();
+ assertThat(metadata.getParameters().get(0).getName()).isEqualTo("id");
+ }
+}
diff --git a/src/test/java/org/springframework/data/mapping/model/KotlinPropertyAccessorFactoryTests.java b/src/test/java/org/springframework/data/mapping/model/KotlinPropertyAccessorFactoryTests.java
new file mode 100644
index 0000000000..0750748b1c
--- /dev/null
+++ b/src/test/java/org/springframework/data/mapping/model/KotlinPropertyAccessorFactoryTests.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2023 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
+ *
+ * https://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 static org.assertj.core.api.Assertions.*;
+
+import kotlin.jvm.JvmClassMappingKt;
+import kotlin.reflect.KClass;
+import kotlin.reflect.jvm.internal.KotlinReflectionInternalError;
+
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.beans.BeanUtils;
+import org.springframework.data.mapping.Parameter;
+import org.springframework.data.mapping.context.SampleMappingContext;
+import org.springframework.data.mapping.context.SamplePersistentProperty;
+
+/**
+ * Kotlin-specific unit tests for {@link ClassGeneratingPropertyAccessorFactory} and
+ * {@link BeanWrapperPropertyAccessorFactory}
+ *
+ * @author John Blum
+ * @author Oliver Gierke
+ * @author Mark Paluch
+ */
+public class KotlinPropertyAccessorFactoryTests {
+
+ private EntityInstantiators instantiators = new EntityInstantiators();
+ private SampleMappingContext mappingContext = new SampleMappingContext();
+
+ @MethodSource("factories")
+ @ParameterizedTest // GH-1947
+ void shouldGeneratePropertyAccessorForTypeWithValueClass(PersistentPropertyAccessorFactory factory) {
+
+ BasicPersistentEntity entity = mappingContext
+ .getRequiredPersistentEntity(WithMyValueClass.class);
+
+ Object instance = createInstance(entity, parameter -> "foo");
+
+ var propertyAccessor = factory.getPropertyAccessor(entity, instance);
+ var persistentProperty = entity.getRequiredPersistentProperty("id");
+
+ assertThat(propertyAccessor).isNotNull();
+ assertThat(propertyAccessor.getProperty(persistentProperty)).isEqualTo("foo");
+
+ if (factory instanceof BeanWrapperPropertyAccessorFactory) {
+
+ // Sigh. Reflection requires a wrapped value while copy accepts the inlined type.
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> propertyAccessor.setProperty(persistentProperty, "bar"));
+ return;
+ }
+
+ propertyAccessor.setProperty(persistentProperty, "bar");
+ assertThat(propertyAccessor.getProperty(persistentProperty)).isEqualTo("bar");
+ }
+
+ @MethodSource("factories")
+ @ParameterizedTest // GH-1947
+ void shouldGeneratePropertyAccessorForTypeWithNullableValueClass(PersistentPropertyAccessorFactory factory)
+ throws ReflectiveOperationException {
+
+ BasicPersistentEntity entity = mappingContext
+ .getRequiredPersistentEntity(WithNestedMyNullableValueClass.class);
+
+ Object instance = createInstance(entity, parameter -> null);
+
+ var propertyAccessor = factory.getPropertyAccessor(entity, instance);
+
+ var expectedDefaultValue = BeanUtils
+ .instantiateClass(MyNullableValueClass.class.getDeclaredConstructor(String.class), "id");
+ var barValue = BeanUtils.instantiateClass(MyNullableValueClass.class.getDeclaredConstructor(String.class), "bar");
+ var property = entity.getRequiredPersistentProperty("baz");
+
+ assertThat(propertyAccessor).isNotNull();
+ assertThat(propertyAccessor.getProperty(property)).isEqualTo(expectedDefaultValue);
+
+ propertyAccessor.setProperty(property, barValue);
+ assertThat(propertyAccessor.getProperty(property)).isEqualTo(barValue);
+ }
+
+ @MethodSource("factories")
+ @ParameterizedTest // GH-1947
+ void shouldGeneratePropertyAccessorForDataClassWithNullableValueClass(PersistentPropertyAccessorFactory factory)
+ throws ReflectiveOperationException {
+
+ BasicPersistentEntity entity = mappingContext
+ .getRequiredPersistentEntity(DataClassWithNullableValueClass.class);
+
+ Object instance = createInstance(entity, parameter -> null);
+
+ var propertyAccessor = factory.getPropertyAccessor(entity, instance);
+
+ var expectedDefaultValue = BeanUtils
+ .instantiateClass(MyNullableValueClass.class.getDeclaredConstructor(String.class), "id");
+ var barValue = BeanUtils.instantiateClass(MyNullableValueClass.class.getDeclaredConstructor(String.class), "bar");
+ var fullyNullable = entity.getRequiredPersistentProperty("fullyNullable");
+
+ assertThat(propertyAccessor).isNotNull();
+ assertThat(propertyAccessor.getProperty(fullyNullable)).isEqualTo(expectedDefaultValue);
+
+ if (factory instanceof BeanWrapperPropertyAccessorFactory) {
+
+ // see https://youtrack.jetbrains.com/issue/KT-57357
+ assertThatExceptionOfType(KotlinReflectionInternalError.class)
+ .isThrownBy(() -> propertyAccessor.setProperty(fullyNullable, barValue))
+ .withMessageContaining("This callable does not support a default call");
+ return;
+ }
+
+ propertyAccessor.setProperty(fullyNullable, barValue);
+ assertThat(propertyAccessor.getProperty(fullyNullable)).isEqualTo(barValue);
+
+ var innerNullable = entity.getRequiredPersistentProperty("innerNullable");
+
+ assertThat(propertyAccessor).isNotNull();
+ assertThat(propertyAccessor.getProperty(innerNullable)).isEqualTo("id");
+
+ propertyAccessor.setProperty(innerNullable, "bar");
+ assertThat(propertyAccessor.getProperty(innerNullable)).isEqualTo("bar");
+ }
+
+ @MethodSource("factories")
+ @ParameterizedTest // GH-1947
+ void nestedNullablePropertiesShouldBeSetCorrectly(PersistentPropertyAccessorFactory factory) {
+
+ BasicPersistentEntity entity = mappingContext
+ .getRequiredPersistentEntity(DataClassWithNestedNullableValueClass.class);
+
+ Object instance = createInstance(entity, parameter -> null);
+
+ var propertyAccessor = factory.getPropertyAccessor(entity, instance);
+ var nullableNestedNullable = entity.getRequiredPersistentProperty("nullableNestedNullable");
+
+ assertThat(propertyAccessor).isNotNull();
+ assertThat(propertyAccessor.getProperty(nullableNestedNullable)).isNull();
+
+ KClass nested = JvmClassMappingKt.getKotlinClass(MyNestedNullableValueClass.class);
+ KClass nullable = JvmClassMappingKt.getKotlinClass(MyNullableValueClass.class);
+
+ MyNullableValueClass inner = nullable.getConstructors().iterator().next().call("new-value");
+ MyNestedNullableValueClass outer = nested.getConstructors().iterator().next().call(inner);
+
+ if (factory instanceof BeanWrapperPropertyAccessorFactory) {
+
+ // see https://youtrack.jetbrains.com/issue/KT-57357
+ assertThatExceptionOfType(KotlinReflectionInternalError.class)
+ .isThrownBy(() -> propertyAccessor.setProperty(nullableNestedNullable, outer))
+ .withMessageContaining("This callable does not support a default call");
+ return;
+ }
+
+ propertyAccessor.setProperty(nullableNestedNullable, outer);
+ assertThat(propertyAccessor.getProperty(nullableNestedNullable)).isInstanceOf(MyNestedNullableValueClass.class)
+ .hasToString("MyNestedNullableValueClass(id=MyNullableValueClass(id=new-value))");
+
+ var nestedNullable = entity.getRequiredPersistentProperty("nestedNullable");
+
+ assertThat(propertyAccessor).isNotNull();
+ assertThat(propertyAccessor.getProperty(nestedNullable)).isNull();
+
+ propertyAccessor.setProperty(nestedNullable, "inner");
+ assertThat(propertyAccessor.getProperty(nestedNullable)).isEqualTo("inner");
+ }
+
+ @MethodSource("factories")
+ @ParameterizedTest // GH-1947
+ void genericInlineClassesShouldWork(PersistentPropertyAccessorFactory factory) {
+
+ BasicPersistentEntity entity = mappingContext
+ .getRequiredPersistentEntity(WithGenericValue.class);
+
+ KClass genericClass = JvmClassMappingKt.getKotlinClass(MyGenericValue.class);
+ MyGenericValue> inner = genericClass.getConstructors().iterator().next().call("initial-value");
+ MyGenericValue> outer = genericClass.getConstructors().iterator().next().call(inner);
+
+ MyGenericValue> newInner = genericClass.getConstructors().iterator().next().call("new-value");
+ MyGenericValue> newOuter = genericClass.getConstructors().iterator().next().call(newInner);
+
+ Object instance = createInstance(entity, parameter -> parameter.getName().equals("recursive") ? outer : "aaa");
+
+ var propertyAccessor = factory.getPropertyAccessor(entity, instance);
+ var string = entity.getRequiredPersistentProperty("string");
+
+ assertThat(propertyAccessor).isNotNull();
+ assertThat(propertyAccessor.getProperty(string)).isEqualTo("aaa");
+
+ if (factory instanceof BeanWrapperPropertyAccessorFactory) {
+
+ // see https://youtrack.jetbrains.com/issue/KT-57357
+ assertThatExceptionOfType(KotlinReflectionInternalError.class)
+ .isThrownBy(() -> propertyAccessor.setProperty(string, "string"))
+ .withMessageContaining("This callable does not support a default call");
+ return;
+ }
+
+ propertyAccessor.setProperty(string, "string");
+ assertThat(propertyAccessor.getProperty(string)).isEqualTo("string");
+
+ var charseq = entity.getRequiredPersistentProperty("charseq");
+
+ assertThat(propertyAccessor).isNotNull();
+ assertThat(propertyAccessor.getProperty(charseq)).isEqualTo("aaa");
+ propertyAccessor.setProperty(charseq, "charseq");
+ assertThat(propertyAccessor.getProperty(charseq)).isEqualTo("charseq");
+
+ var recursive = entity.getRequiredPersistentProperty("recursive");
+
+ assertThat(propertyAccessor).isNotNull();
+ assertThat(propertyAccessor.getProperty(recursive)).isEqualTo(outer);
+ propertyAccessor.setProperty(recursive, newOuter);
+
+ // huh? why is that?
+ assertThat(propertyAccessor.getProperty(recursive)).isEqualTo(newInner);
+ }
+
+ @MethodSource("factories")
+ @ParameterizedTest // GH-1947
+ void genericNullableInlineClassesShouldWork(PersistentPropertyAccessorFactory factory) {
+
+ BasicPersistentEntity entity = mappingContext
+ .getRequiredPersistentEntity(WithGenericNullableValue.class);
+
+ KClass genericClass = JvmClassMappingKt.getKotlinClass(MyGenericValue.class);
+ MyGenericValue> inner = genericClass.getConstructors().iterator().next().call("initial-value");
+ MyGenericValue> outer = genericClass.getConstructors().iterator().next().call(inner);
+
+ MyGenericValue> newInner = genericClass.getConstructors().iterator().next().call("new-value");
+ MyGenericValue> newOuter = genericClass.getConstructors().iterator().next().call(newInner);
+
+ Object instance = createInstance(entity, parameter -> outer);
+
+ var propertyAccessor = factory.getPropertyAccessor(entity, instance);
+ var recursive = entity.getRequiredPersistentProperty("recursive");
+
+ assertThat(propertyAccessor).isNotNull();
+ assertThat(propertyAccessor.getProperty(recursive)).isEqualTo(outer);
+ propertyAccessor.setProperty(recursive, newOuter);
+ assertThat(propertyAccessor.getProperty(recursive)).isEqualTo(newOuter);
+ }
+
+ private Object createInstance(BasicPersistentEntity, SamplePersistentProperty> entity,
+ Function, Object> parameterProvider) {
+ return instantiators.getInstantiatorFor(entity).createInstance(entity,
+ new ParameterValueProvider() {
+ @Override
+ public T getParameterValue(Parameter parameter) {
+ return (T) parameterProvider.apply(parameter);
+ }
+ });
+ }
+
+ static Stream factories() {
+ return Stream.of(new ClassGeneratingPropertyAccessorFactory(), BeanWrapperPropertyAccessorFactory.INSTANCE);
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/mapping/model/ReflectionEntityInstantiatorUnitTests.java b/src/test/java/org/springframework/data/mapping/model/ReflectionEntityInstantiatorUnitTests.java
index 301139c0bb..8f7653b1b8 100755
--- a/src/test/java/org/springframework/data/mapping/model/ReflectionEntityInstantiatorUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/model/ReflectionEntityInstantiatorUnitTests.java
@@ -68,6 +68,7 @@ void instantiatesTypeWithPreferredConstructorUsingParameterValueProvider() {
PreferredConstructor constructor = PreferredConstructorDiscoverer.discover(Foo.class);
+ doReturn(Foo.class).when(entity).getType();
doReturn(constructor).when(entity).getInstanceCreatorMetadata();
var instance = INSTANCE.createInstance(entity, provider);
@@ -78,7 +79,6 @@ void instantiatesTypeWithPreferredConstructorUsingParameterValueProvider() {
}
@Test // DATACMNS-300
- @SuppressWarnings({ "unchecked", "rawtypes" })
void throwsExceptionOnBeanInstantiationException() {
doReturn(PersistentEntity.class).when(entity).getType();
diff --git a/src/test/kotlin/org/springframework/data/mapping/model/InlineClasses.kt b/src/test/kotlin/org/springframework/data/mapping/model/InlineClasses.kt
new file mode 100644
index 0000000000..1e555c9f60
--- /dev/null
+++ b/src/test/kotlin/org/springframework/data/mapping/model/InlineClasses.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2023 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
+ *
+ * https://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.springframework.data.annotation.PersistenceCreator
+import java.time.LocalDate
+
+/**
+ * @author Mark Paluch
+ */
+@JvmInline
+value class MyValueClass(val id: String)
+
+/**
+ * Simple class. Value class is flattened into `String`. However, `copy` requires a boxed value when called via reflection.
+ */
+data class WithMyValueClass(val id: MyValueClass) {
+
+ // ByteCode explanation
+
+ // ---------
+ // default constructor, detected by Discoverers.KOTLIN
+ // private WithMyValueClass(java.lang.String arg0) {}
+ // ---------
+
+ // ---------
+ // synthetic constructor that we actually want to use
+ // public synthetic WithMyValueClass(java.lang.String arg0, kotlin.jvm.internal.DefaultConstructorMarker arg1) {}
+ // ---------
+
+ // ---------
+ // public static WithMyValueClass copy-R7yrDNU$default(WithMyValueClass var0, String var1, int var2, Object var3) {
+ // ---------
+}
+
+@JvmInline
+value class MyNullableValueClass(val id: String? = "id")
+
+@JvmInline
+value class MyNestedNullableValueClass(val id: MyNullableValueClass)
+
+@JvmInline
+value class MyGenericValue(val id: T)
+
+@JvmInline
+value class MyGenericBoundValue(val id: T)
+
+data class WithGenericValue(
+ // ctor: WithGenericValue(CharSequence string, CharSequence charseq, Object recursive, DefaultConstructorMarker $constructor_marker)
+ val string: MyGenericBoundValue,
+ val charseq: MyGenericBoundValue,
+ val recursive: MyGenericValue>
+
+ // copy: copy-XQwFSJ0$default(WithGenericValue var0, CharSequence var1, CharSequence var2, MyGenericValue var3, int var4, Object var5) {
+)
+
+data class WithGenericNullableValue(val recursive: MyGenericValue>?)
+
+@JvmInline
+value class PrimitiveNullableValue(val id: Int?)
+
+data class WithDefaultPrimitiveValue(
+ val pvd: PrimitiveValue = PrimitiveValue(1)
+)
+
+/**
+ * Copy method for nullable value component type uses wrappers while constructor uses the component type.
+ */
+data class WithPrimitiveNullableValue(
+ // ctor: public WithPrimitiveNullableValue(Integer nv, PrimitiveNullableValue nvn, Integer nvd, PrimitiveNullableValue nvdn, DefaultConstructorMarker $constructor_marker) {
+ val nv: PrimitiveNullableValue,
+ val nvn: PrimitiveNullableValue?,
+ val nvd: PrimitiveNullableValue = PrimitiveNullableValue(1),
+ val nvdn: PrimitiveNullableValue? = PrimitiveNullableValue(1),
+
+ // copy: copy-lcs_1S0$default(WithPrimitiveNullableValue var0, PrimitiveNullableValue var1, PrimitiveNullableValue var2, PrimitiveNullableValue var3, PrimitiveNullableValue var4, int var5, Object var6)
+)
+
+@JvmInline
+value class PrimitiveValue(val id: Int)
+
+data class WithPrimitiveValue(
+
+ // ctor: int,org.springframework.data.mapping.model.KotlinValueUtilsUnitTests$PrimitiveValue,int,org.springframework.data.mapping.model.KotlinValueUtilsUnitTests$PrimitiveValue,kotlin.jvm.internal.DefaultConstructorMarker
+ val nv: PrimitiveValue,
+ val nvn: PrimitiveValue?,
+ val nvd: PrimitiveValue = PrimitiveValue(1),
+ val nvdn: PrimitiveValue? = PrimitiveValue(1),
+
+ // copy: copy-XQwFSJ0$default(WithPrimitiveValue var0, int var1, PrimitiveValue var2, int var3, PrimitiveValue var4, int var5, Object var6)
+)
+
+@JvmInline
+value class StringValue(val id: String)
+
+data class WithStringValue(
+
+ // ctor: WithStringValue(String nv, String nvn, String nvd, String nvdn)
+ val nv: StringValue,
+ val nvn: StringValue?,
+ val nvd: StringValue = StringValue("1"),
+ val nvdn: StringValue? = StringValue("1"),
+
+ // copy: copy-QB2wzyg$default(WithStringValue var0, String var1, String var2, String var3, String var4, int var5, Object var6)
+)
+
+
+data class Inner(val id: LocalDate, val foo: String)
+
+@JvmInline
+value class Outer(val id: Inner = Inner(LocalDate.MAX, ""))
+
+data class WithCustomInner(
+
+ // ctor: private WithCustomInner(Inner nv)
+ val nv: Outer
+
+ // copy: copy(org.springframework.data.mapping.model.WithCustomInner,org.springframework.data.mapping.model.Inner,int,java.lang.Object)
+)
+
+class WithNestedMyNullableValueClass(
+ var id: MyNestedNullableValueClass? = MyNestedNullableValueClass(
+ MyNullableValueClass("foo")
+ ), var baz: MyNullableValueClass? = MyNullableValueClass("id")
+
+ // ByteCode explanation
+
+ // ---------
+ // private WithNestedMyNullableValueClass(MyNestedNullableValueClass id, MyNullableValueClass baz) {}
+ // ---------
+
+ // ---------
+ // default constructor, detected by Discoverers.KOTLIN
+ // note that these constructors use boxed variants ("MyNestedNullableValueClass")
+
+ // public synthetic WithNestedMyNullableValueClass(MyNestedNullableValueClass id, MyNullableValueClass baz, DefaultConstructorMarker $constructor_marker) {}
+ // ---------
+
+ // ---------
+ // translated by KotlinInstantiationDelegate.resolveKotlinJvmConstructor as we require a constructor that we can use to
+ // provide the defaulting mask. This constructor only gets generated when parameters are nullable, otherwise
+ // Kotlin doesn't create this constructor
+
+ // public synthetic WithNestedMyNullableValueClass(MyNestedNullableValueClass var1, MyNullableValueClass var2, int var3, DefaultConstructorMarker var4) {}
+ // ---------
+)
+
+/**
+ * Nullability on the domain class means that public and static copy methods uses both the boxed type.
+ */
+data class DataClassWithNullableValueClass(
+
+ val fullyNullable: MyNullableValueClass? = MyNullableValueClass("id"),
+ val innerNullable: MyNullableValueClass = MyNullableValueClass("id")
+
+ // ByteCode
+
+ // private final MyNullableValueClass fullyNullable;
+ // private final String innerNullable;
+
+ // ---------
+ // private DataClassWithNullableValueClass(MyNullableValueClass fullyNullable, String innerNullable)
+ // ---------
+
+ // ---------
+ // public DataClassWithNullableValueClass(MyNullableValueClass var1, MyNullableValueClass var2, int var3, DefaultConstructorMarker var4) {
+ // ---------
+
+ // ---------
+ // public final DataClassWithNullableValueClass copy-bwh045w (@Nullable MyNullableValueClass fullyNullable, @NotNull String innerNullable) {
+ // ---------
+
+ // ---------
+ // public static DataClassWithNullableValueClass copy-bwh045w$default(DataClassWithNullableValueClass var0, MyNullableValueClass var1, MyNullableValueClass var2, int var3, Object var4) {}
+ // ---------
+)
+
+/**
+ * Nullability on a nested level (domain class property isn't nullable, the inner value in the value class is) means that public copy method uses the value component type while the static copy method uses the boxed type.
+ *
+ * This creates a skew in getter vs. setter. The setter would be required to set the boxed value while the getter returns plain String. Sigh.
+ */
+data class DataClassWithNestedNullableValueClass(
+ val nullableNestedNullable: MyNestedNullableValueClass?,
+ val nestedNullable: MyNestedNullableValueClass
+
+ // ByteCode
+
+ // ---------
+ // public DataClassWithNestedNullableValueClass(MyNestedNullableValueClass nullableNestedNullable, String nestedNullable, DefaultConstructorMarker $constructor_marker) {
+ // ---------
+
+ // ---------
+ // public final DataClassWithNestedNullableValueClass copy-W2GYjxM(@Nullable MyNestedNullableValueClass nullableNestedNullable, @NotNull String nestedNullable) { … }
+ // ---------
+
+ // ---------
+ // public static DataClassWithNestedNullableValueClass copy-W2GYjxM$default(DataClassWithNestedNullableValueClass var0, MyNestedNullableValueClass var1, MyNestedNullableValueClass var2, int var3, Object var4) {
+ // ---------
+)
+
+class WithValueClassPreferredConstructor(
+ val id: MyNestedNullableValueClass? = MyNestedNullableValueClass(
+ MyNullableValueClass("foo")
+ ), val baz: MyNullableValueClass? = MyNullableValueClass("id")
+) {
+
+ @PersistenceCreator
+ constructor(
+ a: String, id: MyNestedNullableValueClass? = MyNestedNullableValueClass(
+ MyNullableValueClass("foo")
+ )
+ ) : this(id, MyNullableValueClass(a + "-pref")) {
+
+ }
+
+ // ByteCode explanation
+
+ // ---------
+ // private WithPreferredConstructor(MyNestedNullableValueClass id, MyNullableValueClass baz) {}
+ // ---------
+
+ // ---------
+ // public WithPreferredConstructor(MyNestedNullableValueClass var1, MyNullableValueClass var2, int var3, DefaultConstructorMarker var4) {}
+ // ---------
+
+ // ---------
+ // private WithPreferredConstructor(String a, MyNestedNullableValueClass id) {}
+ // ---------
+
+ // ---------
+ // this is the one we need to invoke to pass on the defaulting mask
+ // public synthetic WithPreferredConstructor(String var1, MyNestedNullableValueClass var2, int var3, DefaultConstructorMarker var4) {}
+ // ---------
+
+ // ---------
+ // public synthetic WithPreferredConstructor(MyNestedNullableValueClass id, MyNullableValueClass baz, DefaultConstructorMarker $constructor_marker) {
+ // ---------
+
+ // ---------
+ // annotated constructor, detected by Discoverers.KOTLIN
+ // @PersistenceCreator
+ // public WithPreferredConstructor(String a, MyNestedNullableValueClass id, DefaultConstructorMarker $constructor_marker) {
+ // ---------
+
+}
+
diff --git a/src/test/kotlin/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiatorUnitTests.kt b/src/test/kotlin/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiatorUnitTests.kt
index 4d03649bf7..5eb67944db 100644
--- a/src/test/kotlin/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiatorUnitTests.kt
+++ b/src/test/kotlin/org/springframework/data/mapping/model/KotlinClassGeneratingEntityInstantiatorUnitTests.kt
@@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test
import org.springframework.data.annotation.PersistenceConstructor
import org.springframework.data.mapping.PersistentEntity
import org.springframework.data.mapping.context.SamplePersistentProperty
+import kotlin.reflect.KClass
/**
* Unit tests for [KotlinClassGeneratingEntityInstantiator] creating instances using Kotlin data classes.
@@ -135,7 +136,8 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests {
every { entity.type } returns constructor!!.constructor.declaringClass
every { entity.typeInformation } returns mockk()
- val instance: WithPrimitiveDefaulting = KotlinClassGeneratingEntityInstantiator().createInstance(entity, provider)
+ val instance: WithPrimitiveDefaulting =
+ KotlinClassGeneratingEntityInstantiator().createInstance(entity, provider)
assertThat(instance.aByte).isEqualTo(0)
assertThat(instance.aShort).isEqualTo(0)
@@ -150,43 +152,124 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests {
@Test // DATACMNS-1338
fun `should create instance using @PersistenceConstructor`() {
- val entity = mockk>()
+ every { provider.getParameterValue(any()) } returns "Walter"
+ val instance = construct(CustomUser::class)
+
+ assertThat(instance.id).isEqualTo("Walter")
+ }
+
+ @Test
+ fun `should use default constructor for types using value class`() {
+
+ every { provider.getParameterValue(any()) } returns "hello"
+ val instance = construct(WithMyValueClass::class)
+
+ assertThat(instance.id.id).isEqualTo("hello")
+ }
+
+ @Test
+ fun `should use default constructor for types using nullable value class`() {
+
+ every { provider.getParameterValue(any()) } returns null
+
+ val instance = construct(WithNestedMyNullableValueClass::class)
+
+ assertThat(instance.id?.id?.id).isEqualTo("foo")
+ assertThat(instance.baz?.id).isEqualTo("id")
+ }
+
+ @Test // GH-1947
+ fun `should use annotated constructor for types using nullable value class`() {
+
+ every { provider.getParameterValue(any()) }.returnsMany("Walter", null)
+
+ val instance = construct(WithValueClassPreferredConstructor::class)
+
+ assertThat(instance.id?.id?.id).isEqualTo("foo")
+ assertThat(instance.baz?.id).isEqualTo("Walter-pref")
+ }
+
+ @Test // GH-1947
+ fun `should instantiate type with value class defaulting`() {
+
+ every { provider.getParameterValue(any()) }.returns(1)
+
+ val instance = construct(WithDefaultPrimitiveValue::class)
+
+ assertThat(instance.pvd.id).isEqualTo(1)
+ }
+
+ @Test // GH-1947
+ fun `should instantiate type with nullable primitive value class defaulting`() {
+
+ every { provider.getParameterValue(any()) }.returnsMany(
+ 1,
+ PrimitiveNullableValue(2),
+ 3,
+ PrimitiveNullableValue(4)
+ )
+
+ val instance = construct(WithPrimitiveNullableValue::class)
+
+ assertThat(instance.nv.id).isEqualTo(1)
+ assertThat(instance.nvn!!.id).isEqualTo(2)
+ assertThat(instance.nvd.id).isEqualTo(3)
+ assertThat(instance.nvdn!!.id).isEqualTo(4)
+ }
+
+ @Test // GH-1947
+ fun `should instantiate type with nullable value class defaulting`() {
+
+ // String nv, String nvn, String nvd, String nvdn, LocalDate dnv, LocalDate dnvn, LocalDate dnvd, LocalDate dnvdn
+ every { provider.getParameterValue(any()) }.returnsMany("1", "2", "3", "4")
+
+ val instance = construct(WithStringValue::class)
+
+ assertThat(instance.nv.id).isEqualTo("1")
+ assertThat(instance.nvn!!.id).isEqualTo("2")
+ assertThat(instance.nvd.id).isEqualTo("3")
+ assertThat(instance.nvdn!!.id).isEqualTo("4")
+ }
+
+ private fun construct(typeToCreate: KClass): T {
+
+ val entity =
+ mockk>()
val constructor =
- PreferredConstructorDiscoverer.discover(
- CustomUser::class.java
+ PreferredConstructorDiscoverer.discover(
+ typeToCreate.java
)
- every { provider.getParameterValue(any()) } returns "Walter"
every { entity.instanceCreatorMetadata } returns constructor
every { entity.type } returns constructor!!.constructor.declaringClass
every { entity.typeInformation } returns mockk()
- val instance: CustomUser =
- KotlinClassGeneratingEntityInstantiator().createInstance(entity, provider)
-
- assertThat(instance.id).isEqualTo("Walter")
+ return KotlinClassGeneratingEntityInstantiator().createInstance(entity, provider)
}
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"
+ 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"
)
data class WithBoolean(val state: Boolean)
- data class WithPrimitiveDefaulting(val aByte: Byte = 0, val aShort: Short = 0, val anInt: Int = 0, val aLong: Long = 0L,
- val aFloat: Float = 0.0f, val aDouble: Double = 0.0, val aChar: Char = 'a', val aBool: Boolean = true)
+ data class WithPrimitiveDefaulting(
+ val aByte: Byte = 0, val aShort: Short = 0, val anInt: Int = 0, val aLong: Long = 0L,
+ val aFloat: Float = 0.0f, val aDouble: Double = 0.0, val aChar: Char = 'a', val aBool: Boolean = true
+ )
data class ContactWithPersistenceConstructor(val firstname: String, val lastname: String) {
@@ -195,14 +278,15 @@ class KotlinClassGeneratingEntityInstantiatorUnitTests {
}
data class CustomUser(
- var id: String? = null,
+ var id: String? = null,
- var organisations: MutableList = mutableListOf()
+ var organisations: MutableList = mutableListOf()
) {
@PersistenceConstructor
constructor(id: String?) : this(id, mutableListOf())
}
data class Organisation(var id: String? = null)
+
}
diff --git a/src/test/kotlin/org/springframework/data/mapping/model/KotlinValueUtilsUnitTests.kt b/src/test/kotlin/org/springframework/data/mapping/model/KotlinValueUtilsUnitTests.kt
new file mode 100644
index 0000000000..79efef7acd
--- /dev/null
+++ b/src/test/kotlin/org/springframework/data/mapping/model/KotlinValueUtilsUnitTests.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2023 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
+ *
+ * https://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.assertThat
+import org.junit.jupiter.api.Test
+import kotlin.reflect.KParameter
+import kotlin.reflect.jvm.javaConstructor
+
+/**
+ * Unit tests for [KotlinValueUtils].
+ * @author Mark Paluch
+ */
+class KotlinValueUtilsUnitTests {
+
+ @Test // GH-1947
+ internal fun considersCustomInnerConstructorRules() {
+
+ val ctor = WithCustomInner::class.constructors.iterator().next();
+ assertThat(ctor.javaConstructor.toString()).contains("(org.springframework.data.mapping.model.Inner,kotlin.jvm.internal.DefaultConstructorMarker)")
+
+ val vh = KotlinValueUtils.getConstructorValueHierarchy(
+ ctor.parameters.iterator().next()
+ )
+
+ assertThat(vh.actualType).isEqualTo(Inner::class.java)
+ assertThat(vh.appliesBoxing()).isFalse
+ }
+
+ @Test // GH-1947
+ internal fun considersCustomInnerCopyRules() {
+
+ val copy = KotlinCopyMethod.findCopyMethod(WithCustomInner::class.java).get();
+ assertThat(copy.syntheticCopyMethod.toString()).contains("(org.springframework.data.mapping.model.WithCustomInner,org.springframework.data.mapping.model.Inner,int,java.lang.Object)")
+
+ val vh = KotlinValueUtils.getCopyValueHierarchy(
+ copy.copyFunction.parameters.get(1)
+ )
+
+ assertThat(vh.parameterType).isEqualTo(Outer::class.java)
+ assertThat(vh.actualType).isEqualTo(Inner::class.java)
+ assertThat(vh.appliesBoxing()).isFalse
+ }
+
+ @Test // GH-1947
+ internal fun inlinesTypesToStringConstructorRules() {
+
+ val ctor = WithStringValue::class.constructors.iterator().next();
+ assertThat(ctor.javaConstructor.toString()).contains("(java.lang.String,java.lang.String,java.lang.String,java.lang.String,kotlin.jvm.internal.DefaultConstructorMarker)")
+
+ for (kParam in ctor.parameters) {
+
+ val vh = KotlinValueUtils.getConstructorValueHierarchy(kParam)
+
+ assertThat(vh.actualType).isEqualTo(String::class.java)
+ assertThat(vh.appliesBoxing()).describedAs(kParam.toString()).isFalse
+ assertThat(vh.wrap("1")).describedAs(kParam.toString()).isEqualTo("1")
+ }
+ }
+
+ @Test // GH-1947
+ internal fun inlinesTypesToStringCopyRules() {
+
+ val copy = KotlinCopyMethod.findCopyMethod(WithStringValue::class.java).get();
+ assertThat(copy.syntheticCopyMethod.toString()).contains("(org.springframework.data.mapping.model.WithStringValue,java.lang.String,java.lang.String,java.lang.String,java.lang.String,int,java.lang.Object)")
+
+ for (kParam in copy.copyFunction.parameters) {
+
+ if (kParam.kind == KParameter.Kind.INSTANCE) {
+ continue
+ }
+
+ val vh = KotlinValueUtils.getCopyValueHierarchy(kParam)
+
+ assertThat(vh.actualType).isEqualTo(String::class.java)
+ assertThat(vh.appliesBoxing()).describedAs(kParam.toString()).isFalse
+ assertThat(vh.wrap("1")).describedAs(kParam.toString()).isEqualTo("1")
+ }
+ }
+
+ @Test // GH-1947
+ internal fun inlinesTypesToIntConstructorRules() {
+
+ val ctor = WithPrimitiveValue::class.constructors.iterator().next();
+ assertThat(ctor.javaConstructor.toString()).contains("(int,org.springframework.data.mapping.model.PrimitiveValue,int,org.springframework.data.mapping.model.PrimitiveValue,kotlin.jvm.internal.DefaultConstructorMarker)")
+
+ val iterator = ctor.parameters.iterator()
+
+ val nv = KotlinValueUtils.getConstructorValueHierarchy(iterator.next());
+ assertThat(nv.actualType).isEqualTo(Int::class.java)
+ assertThat(nv.appliesBoxing()).isFalse
+
+ val nvn = KotlinValueUtils.getConstructorValueHierarchy(iterator.next());
+ assertThat(nvn.actualType).isEqualTo(Int::class.java)
+ assertThat(nvn.appliesBoxing()).isTrue
+ assertThat(nvn.parameterType).isEqualTo(PrimitiveValue::class.java)
+
+ val nvd = KotlinValueUtils.getConstructorValueHierarchy(iterator.next());
+ assertThat(nvd.actualType).isEqualTo(Int::class.java)
+ assertThat(nvd.appliesBoxing()).isFalse
+
+ val nvdn = KotlinValueUtils.getConstructorValueHierarchy(iterator.next());
+ assertThat(nvdn.actualType).isEqualTo(Int::class.java)
+ assertThat(nvn.parameterType).isEqualTo(PrimitiveValue::class.java)
+ assertThat(nvdn.appliesBoxing()).isTrue
+ }
+
+ @Test // GH-1947
+ internal fun inlinesTypesToIntCopyRules() {
+
+ val copy = KotlinCopyMethod.findCopyMethod(WithPrimitiveValue::class.java).get();
+ assertThat(copy.syntheticCopyMethod.toString()).contains("(org.springframework.data.mapping.model.WithPrimitiveValue,int,org.springframework.data.mapping.model.PrimitiveValue,int,org.springframework.data.mapping.model.PrimitiveValue,int,java.lang.Object)")
+
+ val parameters = copy.copyFunction.parameters;
+
+ val nv = KotlinValueUtils.getConstructorValueHierarchy(parameters[1]);
+ assertThat(nv.actualType).isEqualTo(Int::class.java)
+ assertThat(nv.appliesBoxing()).isFalse
+
+ val nvn = KotlinValueUtils.getConstructorValueHierarchy(parameters[2]);
+ assertThat(nvn.actualType).isEqualTo(Int::class.java)
+ assertThat(nvn.appliesBoxing()).isTrue
+ assertThat(nvn.parameterType).isEqualTo(PrimitiveValue::class.java)
+
+ val nvd = KotlinValueUtils.getConstructorValueHierarchy(parameters[3]);
+ assertThat(nvd.actualType).isEqualTo(Int::class.java)
+ assertThat(nvd.appliesBoxing()).isFalse
+
+ val nvdn = KotlinValueUtils.getConstructorValueHierarchy(parameters[4]);
+ assertThat(nvdn.actualType).isEqualTo(Int::class.java)
+ assertThat(nvn.parameterType).isEqualTo(PrimitiveValue::class.java)
+ assertThat(nvdn.appliesBoxing()).isTrue
+ }
+
+ @Test // GH-1947
+ internal fun inlinesGenericTypesConstructorRules() {
+
+ val ctor = WithGenericValue::class.constructors.iterator().next();
+ assertThat(ctor.javaConstructor.toString()).contains("(java.lang.CharSequence,java.lang.CharSequence,java.lang.Object,kotlin.jvm.internal.DefaultConstructorMarker)")
+
+ val iterator = ctor.parameters.iterator()
+
+ val string = KotlinValueUtils.getConstructorValueHierarchy(iterator.next());
+ assertThat(string.actualType).isEqualTo(CharSequence::class.java)
+ assertThat(string.appliesBoxing()).isFalse
+
+ val charseq = KotlinValueUtils.getConstructorValueHierarchy(iterator.next());
+ assertThat(charseq.actualType).isEqualTo(CharSequence::class.java)
+ assertThat(charseq.appliesBoxing()).isFalse
+
+ val recursive = KotlinValueUtils.getConstructorValueHierarchy(iterator.next());
+ assertThat(recursive.actualType).isEqualTo(Any::class.java)
+ assertThat(recursive.appliesBoxing()).isFalse
+ }
+
+ @Test // GH-1947
+ internal fun inlinesGenericTypesCopyRules() {
+
+ val copy = KotlinCopyMethod.findCopyMethod(WithGenericValue::class.java).get();
+ assertThat(copy.syntheticCopyMethod.toString()).contains("(org.springframework.data.mapping.model.WithGenericValue,java.lang.CharSequence,java.lang.CharSequence,org.springframework.data.mapping.model.MyGenericValue,int,java.lang.Object)")
+
+ val parameters = copy.copyFunction.parameters;
+
+ val string = KotlinValueUtils.getConstructorValueHierarchy(parameters[1]);
+ assertThat(string.actualType).isEqualTo(CharSequence::class.java)
+ assertThat(string.appliesBoxing()).isFalse
+
+ val charseq = KotlinValueUtils.getConstructorValueHierarchy(parameters[2]);
+ assertThat(charseq.actualType).isEqualTo(CharSequence::class.java)
+ assertThat(charseq.appliesBoxing()).isFalse
+
+ val recursive = KotlinValueUtils.getConstructorValueHierarchy(parameters[3]);
+ assertThat(recursive.actualType).isEqualTo(Object::class.java)
+ assertThat(recursive.parameterType).isEqualTo(MyGenericValue::class.java)
+ assertThat(recursive.appliesBoxing()).isFalse
+ }
+
+}
+
diff --git a/src/test/kotlin/org/springframework/data/mapping/model/PreferredConstructorDiscovererUnitTests.kt b/src/test/kotlin/org/springframework/data/mapping/model/PreferredConstructorDiscovererUnitTests.kt
index e5b9e43ffa..4d091d91c7 100644
--- a/src/test/kotlin/org/springframework/data/mapping/model/PreferredConstructorDiscovererUnitTests.kt
+++ b/src/test/kotlin/org/springframework/data/mapping/model/PreferredConstructorDiscovererUnitTests.kt
@@ -38,7 +38,8 @@ class PreferredConstructorDiscovererUnitTests {
@Test // DATACMNS-1126
fun `should reject two constructors`() {
- val constructor = PreferredConstructorDiscoverer.discover(TwoConstructors::class.java)
+ val constructor =
+ PreferredConstructorDiscoverer.discover(TwoConstructors::class.java)
assertThat(constructor!!.parameters.size).isEqualTo(1)
}
@@ -46,7 +47,10 @@ class PreferredConstructorDiscovererUnitTests {
@Test // DATACMNS-1170
fun `should fall back to no-args constructor if no primary constructor available`() {
- val constructor = PreferredConstructorDiscoverer.discover(TwoConstructorsWithoutDefault::class.java)
+ val constructor =
+ PreferredConstructorDiscoverer.discover(
+ TwoConstructorsWithoutDefault::class.java
+ )
assertThat(constructor!!.parameters).isEmpty()
}
@@ -54,7 +58,9 @@ class PreferredConstructorDiscovererUnitTests {
@Test // DATACMNS-1126
fun `should discover annotated constructor`() {
- val constructor = PreferredConstructorDiscoverer.discover(AnnotatedConstructors::class.java)
+ val constructor = PreferredConstructorDiscoverer.discover(
+ AnnotatedConstructors::class.java
+ )
assertThat(constructor!!.parameters.size).isEqualTo(2)
}
@@ -62,7 +68,8 @@ class PreferredConstructorDiscovererUnitTests {
@Test // DATACMNS-1126
fun `should discover default constructor`() {
- val constructor = PreferredConstructorDiscoverer.discover(DefaultConstructor::class.java)
+ val constructor =
+ PreferredConstructorDiscoverer.discover(DefaultConstructor::class.java)
assertThat(constructor!!.parameters.size).isEqualTo(1)
}
@@ -70,7 +77,10 @@ class PreferredConstructorDiscovererUnitTests {
@Test // DATACMNS-1126
fun `should discover default annotated constructor`() {
- val constructor = PreferredConstructorDiscoverer.discover(TwoDefaultConstructorsAnnotated::class.java)
+ val constructor =
+ PreferredConstructorDiscoverer.discover(
+ TwoDefaultConstructorsAnnotated::class.java
+ )
assertThat(constructor!!.parameters.size).isEqualTo(3)
}
@@ -88,8 +98,7 @@ class PreferredConstructorDiscovererUnitTests {
assertThat(constructor).isNull()
}
- // See https://github.com/spring-projects/spring-data-commons/issues/2374
- /*@Test // DATACMNS-1800, gh-2215
+ @Test // DATACMNS-1800, GH-2215
@ExperimentalUnsignedTypes
fun `should discover constructor for class using unsigned types`() {
@@ -99,7 +108,7 @@ class PreferredConstructorDiscovererUnitTests {
)
assertThat(constructor).isNotNull()
- } */
+ }
data class Simple(val firstname: String)
@@ -138,13 +147,11 @@ class PreferredConstructorDiscovererUnitTests {
)
}
- /*
- See https://github.com/spring-projects/spring-data-commons/issues/2374
@ExperimentalUnsignedTypes
data class UnsignedTypesEntity(
val id: String,
val a: UInt = 5u,
val b: Int = 5,
val c: Double = 1.5
- ) */
+ )
}
diff --git a/src/test/kotlin/org/springframework/data/mapping/model/ReflectionEntityInstantiatorDataClassUnitTests.kt b/src/test/kotlin/org/springframework/data/mapping/model/ReflectionEntityInstantiatorDataClassUnitTests.kt
index c24f9c2028..44c9a69260 100644
--- a/src/test/kotlin/org/springframework/data/mapping/model/ReflectionEntityInstantiatorDataClassUnitTests.kt
+++ b/src/test/kotlin/org/springframework/data/mapping/model/ReflectionEntityInstantiatorDataClassUnitTests.kt
@@ -21,6 +21,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.data.mapping.PersistentEntity
import org.springframework.data.mapping.context.SamplePersistentProperty
+import kotlin.reflect.KClass
/**
* Unit tests for [ReflectionEntityInstantiator] creating instances using Kotlin data classes.
@@ -36,17 +37,9 @@ class ReflectionEntityInstantiatorDataClassUnitTests {
@Test // DATACMNS-1126
fun `should create instance`() {
- val entity = mockk>()
- val constructor =
- PreferredConstructorDiscoverer.discover(
- Contact::class.java
- )
-
every { provider.getParameterValue(any()) }.returnsMany("Walter", "White")
- every { entity.instanceCreatorMetadata } returns constructor
- val instance: Contact =
- ReflectionEntityInstantiator.INSTANCE.createInstance(entity, provider)
+ val instance: Contact = construct(Contact::class)
assertThat(instance.firstname).isEqualTo("Walter")
assertThat(instance.lastname).isEqualTo("White")
@@ -55,23 +48,39 @@ class ReflectionEntityInstantiatorDataClassUnitTests {
@Test // DATACMNS-1126
fun `should create instance and fill in defaults`() {
- val entity =
- mockk>()
- val constructor =
- PreferredConstructorDiscoverer.discover(
- ContactWithDefaulting::class.java
- )
-
every { provider.getParameterValue(any()) }.returnsMany("Walter", null)
- every { entity.instanceCreatorMetadata } returns constructor
- val instance: ContactWithDefaulting =
- ReflectionEntityInstantiator.INSTANCE.createInstance(entity, provider)
+ val instance: ContactWithDefaulting = construct(ContactWithDefaulting::class)
assertThat(instance.firstname).isEqualTo("Walter")
assertThat(instance.lastname).isEqualTo("White")
}
+ @Test // GH-1947
+ fun `should instantiate type with value class defaulting`() {
+
+ every { provider.getParameterValue(any()) }.returns(1)
+
+ val instance = construct(WithDefaultPrimitiveValue::class)
+
+ assertThat(instance.pvd.id).isEqualTo(1)
+ }
+
+ private fun construct(typeToCreate: KClass): T {
+
+ val entity = mockk>()
+ val constructor =
+ PreferredConstructorDiscoverer.discover(
+ typeToCreate.java
+ )
+
+ every { entity.instanceCreatorMetadata } returns constructor
+ every { entity.type } returns constructor!!.constructor.declaringClass
+ every { entity.typeInformation } returns mockk()
+
+ return ReflectionEntityInstantiator.INSTANCE.createInstance(entity, provider)
+ }
+
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/ReflectionEntityInstantiatorInlineClassUnitTests.kt b/src/test/kotlin/org/springframework/data/mapping/model/ReflectionEntityInstantiatorInlineClassUnitTests.kt
new file mode 100644
index 0000000000..87e4424d73
--- /dev/null
+++ b/src/test/kotlin/org/springframework/data/mapping/model/ReflectionEntityInstantiatorInlineClassUnitTests.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021-2023 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
+ *
+ * https://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 io.mockk.every
+import io.mockk.mockk
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.springframework.data.mapping.PersistentEntity
+import org.springframework.data.mapping.context.SamplePersistentProperty
+import kotlin.reflect.KClass
+
+/**
+ * Unit tests for [ReflectionEntityInstantiator] creating instances using Kotlin inline classes.
+ *
+ * @author Mark Paluch
+ * See also https://github.com/spring-projects/spring-framework/issues/28638
+ */
+class ReflectionEntityInstantiatorValueClassUnitTests {
+
+ val provider = mockk>()
+
+ @Test // GH-1947
+ fun `should create instance`() {
+
+ every { provider.getParameterValue(any()) }.returnsMany("Walter")
+
+ val instance: WithMyValueClass =
+ construct(WithMyValueClass::class)
+
+ assertThat(instance.id.id).isEqualTo("Walter")
+ }
+
+ @Test // GH-1947
+ fun `should create instance with defaulting without value`() {
+
+ every { provider.getParameterValue(any()) } returns null
+
+ val instance: WithNestedMyNullableValueClass = construct(WithNestedMyNullableValueClass::class)
+
+ assertThat(instance.id?.id?.id).isEqualTo("foo")
+ assertThat(instance.baz?.id).isEqualTo("id")
+ }
+
+ @Test // GH-1947
+ fun `should use annotated constructor for types using nullable value class`() {
+
+ every { provider.getParameterValue(any()) }.returnsMany("Walter", null)
+
+ val instance = construct(WithValueClassPreferredConstructor::class)
+
+ assertThat(instance.id?.id?.id).isEqualTo("foo")
+ assertThat(instance.baz?.id).isEqualTo("Walter-pref")
+ }
+
+ private fun construct(typeToCreate: KClass): T {
+
+ val entity = mockk>()
+ val constructor =
+ PreferredConstructorDiscoverer.discover(
+ typeToCreate.java
+ )
+
+ every { entity.instanceCreatorMetadata } returns constructor
+ every { entity.type } returns constructor!!.constructor.declaringClass
+ every { entity.typeInformation } returns mockk()
+
+ return ReflectionEntityInstantiator.INSTANCE.createInstance(entity, provider)
+ }
+
+}
+
diff --git a/src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt b/src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt
new file mode 100644
index 0000000000..5f6fb1de93
--- /dev/null
+++ b/src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023 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
+ *
+ * https://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.util
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.springframework.beans.BeanUtils
+
+/**
+ * Unit tests for [KotlinBeanInfoFactory].
+ * @author Mark Paluch
+ */
+class KotlinBeanInfoFactoryUnitTests {
+
+ @Test
+ internal fun determinesDataClassProperties() {
+
+ val pds = BeanUtils.getPropertyDescriptors(SimpleDataClass::class.java)
+
+ assertThat(pds).hasSize(2).extracting("name").contains("id", "name")
+
+ for (pd in pds) {
+ if (pd.name == "id") {
+ assertThat(pd.readMethod.name).isEqualTo("getId")
+ assertThat(pd.writeMethod).isNull()
+ }
+
+ if (pd.name == "name") {
+ assertThat(pd.readMethod.name).isEqualTo("getName")
+ assertThat(pd.writeMethod.name).isEqualTo("setName")
+ }
+ }
+ }
+
+ @Test
+ internal fun determinesInlineClassConsumerProperties() {
+
+ val pds = BeanUtils.getPropertyDescriptors(WithValueClass::class.java)
+
+ assertThat(pds).hasSize(1).extracting("name").containsOnly("address")
+ assertThat(pds[0].readMethod.name).startsWith("getAddress-") // hashCode suffix
+ assertThat(pds[0].writeMethod.name).startsWith("setAddress-") // hashCode suffix
+ }
+
+ @Test
+ internal fun determinesInlineClassWithDefaultingConsumerProperties() {
+
+ val pds = BeanUtils.getPropertyDescriptors(WithOptionalValueClass::class.java)
+
+ assertThat(pds).hasSize(1).extracting("name").containsOnly("address")
+ assertThat(pds[0].readMethod.name).startsWith("getAddress-") // hashCode suffix
+ assertThat(pds[0].writeMethod).isNull()
+ }
+
+ data class SimpleDataClass(val id: String, var name: String)
+
+ @JvmInline
+ value class EmailAddress(val address: String)
+
+ data class WithValueClass(var address: EmailAddress)
+
+ data class WithOptionalValueClass(val address: EmailAddress? = EmailAddress("un@known"))
+
+}