Add support for Kotlin value classes.

This commit introduces support for Kotlin Value Classes which are designed for a more expressive domain model to make underlying concepts explicit. Spring Data can now read and write types that define properties using Value Classes.
The support covers reflection based instantiation of Kotlin inline class, nullability and defaulting permutations as well as value classes with generics.

Closes: #1947
Original Pull Request: #2866
This commit is contained in:
Mark Paluch
2023-03-30 11:13:09 +02:00
committed by Christoph Strobl
parent 15bb8aa482
commit 80ca2b47ca
24 changed files with 2137 additions and 241 deletions

View File

@@ -33,6 +33,7 @@
<scala>2.11.7</scala>
<xmlbeam>1.4.24</xmlbeam>
<java-module-name>spring.data.commons</java-module-name>
<kotlin.api.target>1.8</kotlin.api.target>
</properties>
@@ -126,6 +127,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin}</version>
</dependency>
<!-- RxJava -->
<dependency>

View File

@@ -436,3 +436,27 @@ You can exclude properties by annotating these with `@Transient`.
2. How to represent properties in your data store?
Using the same field/column name for different values typically leads to corrupt data so you should annotate least one of the properties using an explicit field/column name.
3. Using `@AccessType(PROPERTY)` cannot be used as the super-property cannot be set.
[[mapping.kotlin.value.classes]]
=== Kotlin Value Classes
Kotlin Value Classes are designed for a more expressive domain model to make underlying concepts explicit.
Spring Data can read and write types that define properties using Value Classes.
Consider the following domain model:
====
[source,kotlin]
----
@JvmInline
value class EmailAddress(val theAddress: String) <1>
data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) <2>
----
<1> A simple value class with a non-nullable value type.
<2> Data class defining a property using the `EmailAddress` value class.
====
NOTE: Non-nullable properties using non-primitive value types are flattened in the compiled class to the value type.
Nullable primitive value types or nullable value-in-value types are represented with their wrapper type and that affects how value types are represented in the database.

View File

@@ -168,8 +168,8 @@ class BeanWrapper<T> implements PersistentPropertyAccessor<T> {
KCallable<?> copy = copyMethodCache.computeIfAbsent(type, it -> getCopyMethod(it, property));
if (copy == null) {
throw new UnsupportedOperationException(String.format(
"Kotlin class %s has no .copy(…) method for property %s", type.getName(), property.getName()));
throw new UnsupportedOperationException(String.format("Kotlin class %s has no .copy(…) method for property %s",
type.getName(), property.getName()));
}
return copy.callBy(getCallArgs(copy, property, bean, value));
@@ -179,7 +179,6 @@ class BeanWrapper<T> implements PersistentPropertyAccessor<T> {
T bean, @Nullable Object value) {
Map<KParameter, Object> args = new LinkedHashMap<>(2, 1);
List<KParameter> parameters = callable.getParameters();
for (KParameter parameter : parameters) {
@@ -190,7 +189,8 @@ class BeanWrapper<T> implements PersistentPropertyAccessor<T> {
if (parameter.getKind() == Kind.VALUE && parameter.getName() != null
&& parameter.getName().equals(property.getName())) {
args.put(parameter, value);
args.put(parameter, KotlinValueUtils.getCopyValueHierarchy(parameter).wrap(value));
}
}
return args;

View File

@@ -18,11 +18,15 @@ package org.springframework.data.mapping.model;
import static org.springframework.asm.Opcodes.*;
import static org.springframework.data.mapping.model.BytecodeUtil.*;
import kotlin.reflect.KParameter;
import kotlin.reflect.KParameter.Kind;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collections;
@@ -48,12 +52,16 @@ import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.SimpleAssociationHandler;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.model.KotlinCopyMethod.KotlinCopyByProperty;
import org.springframework.data.mapping.model.KotlinValueUtils.ValueBoxing;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* A factory that can generate byte code to speed-up dynamic property access. Uses the {@link PersistentEntity}'s
@@ -76,6 +84,9 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
private volatile Map<TypeInformation<?>, Class<PersistentPropertyAccessor<?>>> propertyAccessorClasses = new HashMap<>(
32);
private final ConcurrentLruCache<PersistentProperty<?>, Function<Object, Object>> wrapperCache = new ConcurrentLruCache<>(
256, KotlinValueBoxingAdapter::getWrapper);
@Override
public <T> PersistentPropertyAccessor<T> getPropertyAccessor(PersistentEntity<?, ?> entity, T bean) {
@@ -96,7 +107,14 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
args[0] = bean;
try {
return (PersistentPropertyAccessor<T>) constructor.newInstance(args);
PersistentPropertyAccessor<T> accessor = (PersistentPropertyAccessor<T>) constructor.newInstance(args);
if (KotlinDetector.isKotlinType(entity.getType())) {
return new KotlinValueBoxingAdapter<>(entity, accessor, wrapperCache);
}
return accessor;
} catch (Exception e) {
throw new IllegalArgumentException(String.format("Cannot create persistent property accessor for %s", entity), e);
} finally {
@@ -1431,7 +1449,7 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
* Check whether the owning type of {@link PersistentProperty} declares a {@literal copy} method or {@literal copy}
* method with parameter defaulting.
*
* @param type must not be {@literal null}.
* @param property must not be {@literal null}.
* @return
*/
private static boolean hasKotlinCopyMethod(PersistentProperty<?> property) {
@@ -1444,4 +1462,70 @@ public class ClassGeneratingPropertyAccessorFactory implements PersistentPropert
return false;
}
/**
* Adapter to encapsulate Kotlin's value class boxing when properties are nullable.
*
* @param entity the entity that could use value class properties.
* @param delegate the property accessor to delegate to.
* @param wrapperCache cache for wrapping functions.
* @param <T>
* @since 3.2
*/
record KotlinValueBoxingAdapter<T> (PersistentEntity<?, ?> entity, PersistentPropertyAccessor<T> delegate,
ConcurrentLruCache<PersistentProperty<?>, Function<Object, Object>> wrapperCache)
implements
PersistentPropertyAccessor<T> {
@Override
public void setProperty(PersistentProperty<?> property, @Nullable Object value) {
delegate.setProperty(property, wrapperCache.get(property).apply(value));
}
/**
* Create a wrapper function if the {@link PersistentProperty} uses value classes.
*
* @param property the persistent property to inspect.
* @return a wrapper function to wrap a value class component into the hierarchy of value classes or
* {@link Function#identity()} if wrapping is not necessary.
* @see KotlinValueUtils#getCopyValueHierarchy(KParameter)
*/
static Function<Object, Object> getWrapper(PersistentProperty<?> property) {
Optional<KotlinCopyMethod> kotlinCopyMethod = KotlinCopyMethod.findCopyMethod(property.getOwner().getType())
.filter(it -> it.supportsProperty(property));
if (kotlinCopyMethod.isPresent()
&& kotlinCopyMethod.filter(it -> it.forProperty(property).isPresent()).isPresent()) {
KotlinCopyMethod copyMethod = kotlinCopyMethod.get();
Optional<KParameter> kParameter = kotlinCopyMethod.stream()
.flatMap(it -> it.getCopyFunction().getParameters().stream()) //
.filter(kf -> kf.getKind() == Kind.VALUE) //
.filter(kf -> StringUtils.hasText(kf.getName())) //
.filter(kf -> kf.getName().equals(property.getName())) //
.findFirst();
ValueBoxing vh = kParameter.map(KotlinValueUtils::getCopyValueHierarchy).orElse(null);
KotlinCopyByProperty kotlinCopyByProperty = copyMethod.forProperty(property).get();
Method copy = copyMethod.getSyntheticCopyMethod();
Parameter parameter = copy.getParameters()[kotlinCopyByProperty.getParameterPosition()];
return o -> ClassUtils.isAssignableValue(parameter.getType(), o) || vh == null ? o : vh.wrap(o);
}
return Function.identity();
}
@Override
public Object getProperty(PersistentProperty<?> property) {
return delegate.getProperty(property);
}
@Override
public T getBean() {
return delegate.getBean();
}
}
}

View File

@@ -15,6 +15,11 @@
*/
package org.springframework.data.mapping.model;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KCallable;
import kotlin.reflect.KClass;
import kotlin.reflect.KProperty;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
@@ -24,6 +29,7 @@ import java.util.ArrayList;
import java.util.List;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.KotlinDetector;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.annotation.PersistenceCreator;
@@ -55,7 +61,8 @@ class InstanceCreatorMetadataDiscoverer {
* @return
*/
@Nullable
public static <T, P extends PersistentProperty<P>> InstanceCreatorMetadata<P> discover(PersistentEntity<T, P> entity) {
public static <T, P extends PersistentProperty<P>> InstanceCreatorMetadata<P> discover(
PersistentEntity<T, P> entity) {
Constructor<?>[] declaredConstructors = entity.getType().getDeclaredConstructors();
Method[] declaredMethods = entity.getType().getDeclaredMethods();
@@ -78,6 +85,34 @@ class InstanceCreatorMetadataDiscoverer {
}
}
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<TypeInformation<?>> types = entity.getTypeInformation().getParameterTypes(declaredMethod);
return new FactoryMethod<>(declaredMethod,
new Parameter<>(propertyName, (TypeInformation) types.get(0), parameterAnnotations[0], entity));
}
}
}
}
return PreferredConstructorDiscoverer.discover(entity);
}

View File

@@ -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 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta
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 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta
static class DefaultingKotlinClassInstantiatorAdapter implements EntityInstantiator {
private final ObjectInstantiator instantiator;
private final KFunction<?> constructor;
private final List<KParameter> 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 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta
public <T, E extends PersistentEntity<? extends T, P>, P extends PersistentProperty<P>> T createInstance(E entity,
ParameterValueProvider<P> 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 @@ class KotlinClassGeneratingEntityInstantiator extends ClassGeneratingEntityInsta
}
}
private <P extends PersistentProperty<P>, T> Object[] extractInvocationArguments(
@Nullable InstanceCreatorMetadata<P> entityCreator, ParameterValueProvider<P> 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<Parameter<Object, P>> parameters = entityCreator.getParameters();
// Prepare user-space arguments
for (int i = 0; i < userParameterCount; i++) {
Parameter<Object, P> parameter = parameters.get(i);
params[i] = provider.getParameterValue(parameter);
}
KotlinDefaultMask defaultMask = KotlinDefaultMask.from(constructor, it -> {
int index = kParameters.indexOf(it);
Parameter<Object, P> parameter = parameters.get(index);
Class<Object> 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;
}
}
}

View File

@@ -31,6 +31,7 @@ import java.util.ArrayList;
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 @@ class KotlinCopyMethod {
}
/**
* 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 @@ class KotlinCopyMethod {
return Optional.empty();
}
boolean usesValueClasses = KotlinValueUtils.hasValueClassProperty(type);
List<KParameter> constructorArguments = getComponentArguments(primaryConstructor);
Predicate<String> 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 @@ class KotlinCopyMethod {
return Optional.empty();
}
boolean usesValueClasses = KotlinValueUtils.hasValueClassProperty(type);
Predicate<String> 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 @@ class KotlinCopyMethod {
List<KParameter> 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 @@ class KotlinCopyMethod {
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 @@ class KotlinCopyMethod {
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) {

View File

@@ -54,12 +54,23 @@ public class KotlinDefaultMask {
* 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 class KotlinDefaultMask {
* @return {@link KotlinDefaultMask}.
*/
public static KotlinDefaultMask from(KFunction<?> function, Predicate<KParameter> 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<KParameter> 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<KParameter> 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<KParameter> isPresent,
boolean requiresAtLeastOneMask) {
List<Integer> masks = new ArrayList<>();
int index = 0;
int mask = 0;
boolean hasSeenParameter = false;
List<KParameter> parameters = function.getParameters();
@@ -83,8 +131,12 @@ public class KotlinDefaultMask {
mask = 0;
}
if (parameter.isOptional() && !isPresent.test(parameter)) {
mask = mask | (1 << (index % Integer.SIZE));
if (parameter.isOptional()) {
hasSeenParameter = true;
if (!isPresent.test(parameter)) {
mask = mask | (1 << (index % Integer.SIZE));
}
}
if (parameter.getKind() == Kind.VALUE) {
@@ -92,7 +144,9 @@ public class KotlinDefaultMask {
}
}
masks.add(mask);
if (hasSeenParameter || requiresAtLeastOneMask) {
masks.add(mask);
}
return new KotlinDefaultMask(masks.stream().mapToInt(i -> i).toArray());
}

View File

@@ -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<KParameter> kParameters;
private final List<Function<Object, Object>> 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 <P>
*/
public <P extends PersistentProperty<P>> Object[] extractInvocationArguments(Object[] params,
@Nullable InstanceCreatorMetadata<P> entityCreator, ParameterValueProvider<P> provider) {
if (entityCreator == null) {
throw new IllegalArgumentException("EntityCreator must not be null");
}
int userParameterCount = kParameters.size();
List<Parameter<Object, P>> parameters = entityCreator.getParameters();
// Prepare user-space arguments
for (int i = 0; i < userParameterCount; i++) {
Parameter<Object, P> parameter = parameters.get(i);
params[i] = provider.getParameterValue(parameter);
}
KotlinDefaultMask defaultMask = KotlinDefaultMask.forConstructor(constructor, it -> {
int index = kParameters.indexOf(it);
Parameter<Object, P> parameter = parameters.get(index);
Class<Object> 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);
}
}

View File

@@ -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<KProperty<?>> 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<KProperty<?>> getProperties(KClass<?> kClass) {
if (kClass.isValue()) {
for (KCallable<?> member : kClass.getMembers()) {
if (member instanceof KProperty<?> kp) {
return Collections.singletonList(kp);
}
}
}
List<KProperty<?>> 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();
}
}
}

View File

@@ -26,6 +26,7 @@ import java.lang.reflect.RecordComponent;
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 @@ public interface PreferredConstructorDiscoverer {
Class<?> rawOwningType = type.getType();
// Kotlin can rewrite annotated constructors into synthetic ones so we need to inspect that first.
Optional<PreferredConstructor<T, P>> 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

View File

@@ -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 @@ enum ReflectionEntityInstantiator implements EntityInstantiator {
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 @@ enum ReflectionEntityInstantiator implements EntityInstantiator {
}
}
@SuppressWarnings("unchecked")
private static <T, E extends PersistentEntity<? extends T, P>, P extends PersistentProperty<P>> T instantiateKotlinClass(
E entity, ParameterValueProvider<P> 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 <T, E extends PersistentEntity<? extends T, P>, P extends PersistentProperty<P>> T instantiateClass(
E entity) {

View File

@@ -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<PropertyDescriptor> 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.
}
}

View File

@@ -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

View File

@@ -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<SamplePersistentProperty> metadata = InstanceCreatorMetadataDiscoverer
.discover(new BasicPersistentEntity<Object, SamplePersistentProperty>(
(TypeInformation) TypeInformation.of(MyNullableValueClass.class)));
assertThat(metadata).isNotNull();
assertThat(metadata.hasParameters()).isTrue();
assertThat(metadata.getParameters().get(0).getName()).isEqualTo("id");
}
}

View File

@@ -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<Object, SamplePersistentProperty> 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<Object, SamplePersistentProperty> 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<Object, SamplePersistentProperty> 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<Object, SamplePersistentProperty> 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<MyNestedNullableValueClass> nested = JvmClassMappingKt.getKotlinClass(MyNestedNullableValueClass.class);
KClass<MyNullableValueClass> 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<Object, SamplePersistentProperty> entity = mappingContext
.getRequiredPersistentEntity(WithGenericValue.class);
KClass<MyGenericValue> 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<Object, SamplePersistentProperty> entity = mappingContext
.getRequiredPersistentEntity(WithGenericNullableValue.class);
KClass<MyGenericValue> 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<Parameter<?, ?>, Object> parameterProvider) {
return instantiators.getInstantiatorFor(entity).createInstance(entity,
new ParameterValueProvider<SamplePersistentProperty>() {
@Override
public <T> T getParameterValue(Parameter<T, SamplePersistentProperty> parameter) {
return (T) parameterProvider.apply(parameter);
}
});
}
static Stream<PersistentPropertyAccessorFactory> factories() {
return Stream.of(new ClassGeneratingPropertyAccessorFactory(), BeanWrapperPropertyAccessorFactory.INSTANCE);
}
}

View File

@@ -68,6 +68,7 @@ class ReflectionEntityInstantiatorUnitTests<P extends PersistentProperty<P>> {
PreferredConstructor<Foo, P> 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 @@ class ReflectionEntityInstantiatorUnitTests<P extends PersistentProperty<P>> {
}
@Test // DATACMNS-300
@SuppressWarnings({ "unchecked", "rawtypes" })
void throwsExceptionOnBeanInstantiationException() {
doReturn(PersistentEntity.class).when(entity).getType();

View File

@@ -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<T>(val id: T)
@JvmInline
value class MyGenericBoundValue<T : CharSequence>(val id: T)
data class WithGenericValue(
// ctor: WithGenericValue(CharSequence string, CharSequence charseq, Object recursive, DefaultConstructorMarker $constructor_marker)
val string: MyGenericBoundValue<String>,
val charseq: MyGenericBoundValue<CharSequence>,
val recursive: MyGenericValue<MyGenericValue<String>>
// copy: copy-XQwFSJ0$default(WithGenericValue var0, CharSequence var1, CharSequence var2, MyGenericValue var3, int var4, Object var5) {
)
data class WithGenericNullableValue(val recursive: MyGenericValue<MyGenericValue<String>>?)
@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) {
// ---------
}

View File

@@ -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<PersistentEntity<CustomUser, SamplePersistentProperty>>()
val constructor =
PreferredConstructorDiscoverer.discover<CustomUser, SamplePersistentProperty>(
CustomUser::class.java
)
every { provider.getParameterValue<String>(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)
val instance = construct(CustomUser::class)
assertThat(instance.id).isEqualTo("Walter")
}
@Test
fun `should use default constructor for types using value class`() {
every { provider.getParameterValue<String>(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<String>(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<String>(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<Int>(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>(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>(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 <T : Any> construct(typeToCreate: KClass<T>): T {
val entity =
mockk<PersistentEntity<T, SamplePersistentProperty>>()
val constructor =
PreferredConstructorDiscoverer.discover<T, SamplePersistentProperty>(
typeToCreate.java
)
every { entity.instanceCreatorMetadata } returns constructor
every { entity.type } returns constructor!!.constructor.declaringClass
every { entity.typeInformation } returns mockk()
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<Organisation> = mutableListOf()
var organisations: MutableList<Organisation> = mutableListOf()
) {
@PersistenceConstructor
constructor(id: String?) : this(id, mutableListOf())
}
data class Organisation(var id: String? = null)
}

View File

@@ -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
}
}

View File

@@ -38,7 +38,8 @@ class PreferredConstructorDiscovererUnitTests {
@Test // DATACMNS-1126
fun `should reject two constructors`() {
val constructor = PreferredConstructorDiscoverer.discover<TwoConstructors, SamplePersistentProperty>(TwoConstructors::class.java)
val constructor =
PreferredConstructorDiscoverer.discover<TwoConstructors, SamplePersistentProperty>(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, SamplePersistentProperty>(TwoConstructorsWithoutDefault::class.java)
val constructor =
PreferredConstructorDiscoverer.discover<TwoConstructorsWithoutDefault, SamplePersistentProperty>(
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, SamplePersistentProperty>(AnnotatedConstructors::class.java)
val constructor = PreferredConstructorDiscoverer.discover<AnnotatedConstructors, SamplePersistentProperty>(
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, SamplePersistentProperty>(DefaultConstructor::class.java)
val constructor =
PreferredConstructorDiscoverer.discover<DefaultConstructor, SamplePersistentProperty>(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, SamplePersistentProperty>(TwoDefaultConstructorsAnnotated::class.java)
val constructor =
PreferredConstructorDiscoverer.discover<TwoDefaultConstructorsAnnotated, SamplePersistentProperty>(
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
) */
)
}

View File

@@ -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<PersistentEntity<Contact, SamplePersistentProperty>>()
val constructor =
PreferredConstructorDiscoverer.discover<Contact, SamplePersistentProperty>(
Contact::class.java
)
every { provider.getParameterValue<String>(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<PersistentEntity<ContactWithDefaulting, SamplePersistentProperty>>()
val constructor =
PreferredConstructorDiscoverer.discover<ContactWithDefaulting, SamplePersistentProperty>(
ContactWithDefaulting::class.java
)
every { provider.getParameterValue<String>(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<Int>(any()) }.returns(1)
val instance = construct(WithDefaultPrimitiveValue::class)
assertThat(instance.pvd.id).isEqualTo(1)
}
private fun <T : Any> construct(typeToCreate: KClass<T>): T {
val entity = mockk<PersistentEntity<T, SamplePersistentProperty>>()
val constructor =
PreferredConstructorDiscoverer.discover<T, SamplePersistentProperty>(
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")

View File

@@ -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<ParameterValueProvider<SamplePersistentProperty>>()
@Test // GH-1947
fun `should create instance`() {
every { provider.getParameterValue<String>(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<String>(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<String>(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 <T : Any> construct(typeToCreate: KClass<T>): T {
val entity = mockk<PersistentEntity<T, SamplePersistentProperty>>()
val constructor =
PreferredConstructorDiscoverer.discover<T, SamplePersistentProperty>(
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)
}
}

View File

@@ -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"))
}