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:
committed by
Christoph Strobl
parent
15bb8aa482
commit
80ca2b47ca
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user