diff --git a/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java b/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java index 3bd3ee60b..5ca87a38f 100644 --- a/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java +++ b/src/main/java/org/springframework/data/mapping/model/BeanWrapper.java @@ -25,6 +25,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.springframework.core.KotlinDetector; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -74,7 +75,7 @@ class BeanWrapper implements PersistentPropertyAccessor { return; } - if (KotlinReflectionUtils.isDataClass(property.getOwner().getType())) { + if (KotlinDetector.isKotlinPresent() && KotlinReflectionUtils.isDataClass(property.getOwner().getType())) { this.bean = (T) KotlinCopyUtil.setProperty(property, bean, value); return; @@ -154,8 +155,6 @@ class BeanWrapper implements PersistentPropertyAccessor { */ static class KotlinCopyUtil { - private static final Map, KCallable> copyMethodCache = new ConcurrentReferenceHashMap<>(); - /** * Set a single property by calling {@code copy(…)} on a Kotlin data class. Copying creates a new instance that * holds all values of the original instance and the newly set {@link PersistentProperty} value. @@ -165,7 +164,7 @@ class BeanWrapper implements PersistentPropertyAccessor { static Object setProperty(PersistentProperty property, T bean, @Nullable Object value) { Class type = property.getOwner().getType(); - KCallable copy = copyMethodCache.computeIfAbsent(type, it -> getCopyMethod(it, property)); + KCallable copy = getCopyMethod(type, property); if (copy == null) { throw new UnsupportedOperationException(String.format("Kotlin class %s has no .copy(…) method for property %s", diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java b/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java index 0794dbdad..404fc2cd5 100644 --- a/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java +++ b/src/main/java/org/springframework/data/mapping/model/KotlinCopyMethod.java @@ -30,6 +30,7 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -38,17 +39,22 @@ import org.springframework.core.ResolvableType; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.util.KotlinReflectionUtils; import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; /** * Value object to represent a Kotlin {@code copy} method. The lookup requires a {@code copy} method that matches the * primary constructor of the class regardless of whether the primary constructor is the persistence constructor. * * @author Mark Paluch + * @author Christoph Strobl * @since 2.1 */ class KotlinCopyMethod { + private static final Map, Optional> COPY_METHOD_CACHE = new ConcurrentReferenceHashMap<>(); + private final Method publicCopyMethod; private final Method syntheticCopyMethod; private final int parameterCount; @@ -78,15 +84,17 @@ class KotlinCopyMethod { Assert.notNull(type, "Type must not be null"); - Optional syntheticCopyMethod = findSyntheticCopyMethod(type); + return COPY_METHOD_CACHE.computeIfAbsent(type, it -> { - if (!syntheticCopyMethod.isPresent()) { - return Optional.empty(); - } + Optional syntheticCopyMethod = findSyntheticCopyMethod(type); - Optional publicCopyMethod = syntheticCopyMethod.flatMap(KotlinCopyMethod::findPublicCopyMethod); + if (!syntheticCopyMethod.isPresent()) { + return Optional.empty(); + } - return publicCopyMethod.map(method -> new KotlinCopyMethod(method, syntheticCopyMethod.get())); + Optional publicCopyMethod = syntheticCopyMethod.flatMap(KotlinCopyMethod::findPublicCopyMethod); + return publicCopyMethod.map(method -> new KotlinCopyMethod(method, syntheticCopyMethod.get())); + }); } public Method getPublicCopyMethod() { @@ -171,7 +179,7 @@ class KotlinCopyMethod { return Optional.empty(); } - boolean usesValueClasses = KotlinValueUtils.hasValueClassProperty(type); + boolean usesValueClasses = KotlinReflectionUtils.hasValueClassProperty(type); List constructorArguments = getComponentArguments(primaryConstructor); Predicate isCopyMethod; @@ -242,7 +250,7 @@ class KotlinCopyMethod { return Optional.empty(); } - boolean usesValueClasses = KotlinValueUtils.hasValueClassProperty(type); + boolean usesValueClasses = KotlinReflectionUtils.hasValueClassProperty(type); Predicate isCopyMethod = usesValueClasses ? (it -> it.startsWith("copy-") && it.endsWith("$default")) : (it -> it.equals("copy$default")); @@ -277,7 +285,7 @@ class KotlinCopyMethod { KParameter kParameter = constructorArguments.get(i); - if (KotlinValueUtils.isValueClass(kParameter.getType())) { + if (KotlinReflectionUtils.isValueClass(kParameter.getType())) { // sigh. This can require deep unwrapping because the public vs. the synthetic copy methods use different // parameter types. continue; diff --git a/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java b/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java index 02ea07b77..3972b3d3b 100644 --- a/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java +++ b/src/main/java/org/springframework/data/mapping/model/KotlinValueUtils.java @@ -31,7 +31,6 @@ 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; @@ -43,39 +42,6 @@ import org.springframework.util.Assert; */ 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. * @@ -122,9 +88,10 @@ class KotlinValueUtils { 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) { + + boolean isPrimitive = javaType instanceof Class c && c.isPrimitive(); return (isPrimitive && type.isMarkedNullable()) || component.getType().isMarkedNullable(); } diff --git a/src/main/java/org/springframework/data/mapping/model/MappingInstantiationException.java b/src/main/java/org/springframework/data/mapping/model/MappingInstantiationException.java index e6bbcbcb9..4f0e34dfc 100644 --- a/src/main/java/org/springframework/data/mapping/model/MappingInstantiationException.java +++ b/src/main/java/org/springframework/data/mapping/model/MappingInstantiationException.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import org.springframework.core.KotlinDetector; import org.springframework.data.mapping.FactoryMethod; import org.springframework.data.mapping.InstanceCreatorMetadata; import org.springframework.data.mapping.PersistentEntity; @@ -117,7 +118,7 @@ public class MappingInstantiationException extends RuntimeException { Constructor constructor = preferredConstructor.getConstructor(); - if (KotlinReflectionUtils.isSupportedKotlinClass(constructor.getDeclaringClass())) { + if (KotlinDetector.isKotlinPresent() && KotlinReflectionUtils.isSupportedKotlinClass(constructor.getDeclaringClass())) { KFunction kotlinFunction = ReflectJvmMapping.getKotlinFunction(constructor); @@ -133,7 +134,7 @@ public class MappingInstantiationException extends RuntimeException { Method constructor = factoryMethod.getFactoryMethod(); - if (KotlinReflectionUtils.isSupportedKotlinClass(constructor.getDeclaringClass())) { + if (KotlinDetector.isKotlinPresent() && KotlinReflectionUtils.isSupportedKotlinClass(constructor.getDeclaringClass())) { KFunction kotlinFunction = ReflectJvmMapping.getKotlinFunction(constructor); diff --git a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java index 2d1e32150..2c20ced2c 100644 --- a/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java +++ b/src/main/java/org/springframework/data/mapping/model/PreferredConstructorDiscoverer.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Optional; import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.KotlinDetector; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.annotation.PersistenceCreator; @@ -220,6 +221,10 @@ public interface PreferredConstructorDiscoverer { * @return the appropriate discoverer for {@code type}. */ private static Discoverers findDiscoverer(Class type) { + + if(!KotlinDetector.isKotlinPresent()) { + return DEFAULT; + } return KotlinReflectionUtils.isSupportedKotlinClass(type) ? KOTLIN : DEFAULT; } diff --git a/src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java b/src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java index c2849d27e..d3cf1fa66 100644 --- a/src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java +++ b/src/main/java/org/springframework/data/repository/core/support/MethodInvocationValidator.java @@ -22,6 +22,7 @@ import java.util.Map; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.dao.EmptyResultDataAccessException; @@ -58,7 +59,7 @@ public class MethodInvocationValidator implements MethodInterceptor { */ public static boolean supports(Class repositoryInterface) { - return KotlinReflectionUtils.isSupportedKotlinClass(repositoryInterface) + return KotlinDetector.isKotlinPresent() && KotlinReflectionUtils.isSupportedKotlinClass(repositoryInterface) || NullableUtils.isNonNull(repositoryInterface, ElementType.METHOD) || NullableUtils.isNonNull(repositoryInterface, ElementType.PARAMETER); } diff --git a/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java b/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java index 56bc7c26e..1b14f7638 100644 --- a/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/KotlinReflectionUtils.java @@ -37,7 +37,7 @@ import org.springframework.lang.Nullable; /** * Reflection utility methods specific to Kotlin reflection. Requires Kotlin classes to be present to avoid linkage - * errors. + * errors - ensure to guard usage with {@link KotlinDetector#isKotlinPresent()}. * * @author Mark Paluch * @author Christoph Strobl @@ -132,6 +132,42 @@ public final class KotlinReflectionUtils { return JvmClassMappingKt.getJavaClass(KTypesJvm.getJvmErasure(kotlinFunction.getReturnType())); } + /** + * 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. + * @since 3.2 + */ + 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. + * @since 3.2 + */ + 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; + } + /** * Returns {@literal} whether the given {@link MethodParameter} is nullable. Its declaring method can reference a * Kotlin function, property or interface property. diff --git a/src/main/java/org/springframework/data/util/Predicates.java b/src/main/java/org/springframework/data/util/Predicates.java index c7f3e9344..c4e12bf3c 100644 --- a/src/main/java/org/springframework/data/util/Predicates.java +++ b/src/main/java/org/springframework/data/util/Predicates.java @@ -21,6 +21,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.function.Predicate; +import org.springframework.core.KotlinDetector; import org.springframework.util.Assert; /** @@ -45,7 +46,7 @@ public interface Predicates { Predicate IS_PUBLIC = member -> Modifier.isPublic(member.getModifiers()); Predicate IS_SYNTHETIC = Member::isSynthetic; - Predicate> IS_KOTLIN = KotlinReflectionUtils::isSupportedKotlinClass; + Predicate> IS_KOTLIN = KotlinDetector.isKotlinPresent() ? KotlinReflectionUtils::isSupportedKotlinClass : type -> false; Predicate IS_STATIC = member -> Modifier.isStatic(member.getModifiers()); Predicate IS_BRIDGE_METHOD = Method::isBridge; diff --git a/src/test/kotlin/org/springframework/data/mapping/model/InlineClasses.kt b/src/test/kotlin/org/springframework/data/mapping/model/InlineClasses.kt index 1e555c9f6..7c28aaa8f 100644 --- a/src/test/kotlin/org/springframework/data/mapping/model/InlineClasses.kt +++ b/src/test/kotlin/org/springframework/data/mapping/model/InlineClasses.kt @@ -89,6 +89,25 @@ data class WithPrimitiveNullableValue( // copy: copy-lcs_1S0$default(WithPrimitiveNullableValue var0, PrimitiveNullableValue var1, PrimitiveNullableValue var2, PrimitiveNullableValue var3, PrimitiveNullableValue var4, int var5, Object var6) ) +@JvmInline +value class PrimitiveArrayValue(val ids: IntArray) + +@JvmInline +value class PrimitiveNullableArrayValue(val ids: IntArray?) + +data class WithPrimitiveArrays( + + // ctor WithPrimitiveArrays(int[], int[], int[], int[], int[], int, DefaultConstructorMarker) + + val pa: PrimitiveArrayValue, + val pan: PrimitiveArrayValue?, + val pna: PrimitiveNullableArrayValue, + val pad: PrimitiveArrayValue = PrimitiveArrayValue(intArrayOf(1, 2, 3)), + val pand: PrimitiveArrayValue? = PrimitiveArrayValue(intArrayOf(1, 2, 3)) + + // copy: copy-NCSWWqw$default(WithPrimitiveArrays var0, int[] var1, int[] var2, PrimitiveNullableArrayValue var3, int[] var4, int[] var5, int var4, Object var5) { +) + @JvmInline value class PrimitiveValue(val id: Int) diff --git a/src/test/kotlin/org/springframework/data/mapping/model/KotlinValueUtilsUnitTests.kt b/src/test/kotlin/org/springframework/data/mapping/model/KotlinValueUtilsUnitTests.kt index 79efef7ac..46cc688db 100644 --- a/src/test/kotlin/org/springframework/data/mapping/model/KotlinValueUtilsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/mapping/model/KotlinValueUtilsUnitTests.kt @@ -145,6 +145,65 @@ class KotlinValueUtilsUnitTests { assertThat(nvdn.appliesBoxing()).isTrue } + @Test // GH-1947 + internal fun inlinesTypesToPrimitiveArrayCopyRules() { + + val copy = KotlinCopyMethod.findCopyMethod(WithPrimitiveArrays::class.java).get(); + assertThat(copy.syntheticCopyMethod.toString()).contains("(org.springframework.data.mapping.model.WithPrimitiveArrays,int[],int[],org.springframework.data.mapping.model.PrimitiveNullableArrayValue,int[],int[],int,java.lang.Object)") + + val parameters = copy.copyFunction.parameters; + + val pa = KotlinValueUtils.getConstructorValueHierarchy(parameters[1]); + assertThat(pa.actualType).isEqualTo(IntArray::class.java) + assertThat(pa.appliesBoxing()).isFalse + + val pan = KotlinValueUtils.getConstructorValueHierarchy(parameters[2]); + assertThat(pan.actualType).isEqualTo(IntArray::class.java) + assertThat(pan.appliesBoxing()).isFalse + + val pna = KotlinValueUtils.getConstructorValueHierarchy(parameters[3]); + assertThat(pna.actualType).isEqualTo(IntArray::class.java) + assertThat(pna.parameterType).isEqualTo(PrimitiveNullableArrayValue::class.java) + assertThat(pna.appliesBoxing()).isTrue + + val pad = KotlinValueUtils.getConstructorValueHierarchy(parameters[4]); + assertThat(pad.actualType).isEqualTo(IntArray::class.java) + assertThat(pad.appliesBoxing()).isFalse + + val pand = KotlinValueUtils.getConstructorValueHierarchy(parameters[5]); + assertThat(pand.actualType).isEqualTo(IntArray::class.java) + assertThat(pand.appliesBoxing()).isFalse + } + + @Test // GH-1947 + internal fun inlinesPrimitiveArrayConstructorRules() { + + val ctor = WithPrimitiveArrays::class.constructors.iterator().next(); + assertThat(ctor.javaConstructor.toString()).contains("(int[],int[],int[],int[],int[],kotlin.jvm.internal.DefaultConstructorMarker)") + + val iterator = ctor.parameters.iterator() + + val pa = KotlinValueUtils.getConstructorValueHierarchy(iterator.next()); + assertThat(pa.actualType).isEqualTo(IntArray::class.java) + assertThat(pa.appliesBoxing()).isFalse + + val pan = KotlinValueUtils.getConstructorValueHierarchy(iterator.next()); + assertThat(pan.actualType).isEqualTo(IntArray::class.java) + assertThat(pan.appliesBoxing()).isFalse + + val pna = KotlinValueUtils.getConstructorValueHierarchy(iterator.next()); + assertThat(pna.actualType).isEqualTo(IntArray::class.java) + assertThat(pna.appliesBoxing()).isFalse + + val pad = KotlinValueUtils.getConstructorValueHierarchy(iterator.next()); + assertThat(pad.actualType).isEqualTo(IntArray::class.java) + assertThat(pad.appliesBoxing()).isFalse + + val pand = KotlinValueUtils.getConstructorValueHierarchy(iterator.next()); + assertThat(pand.actualType).isEqualTo(IntArray::class.java) + assertThat(pand.appliesBoxing()).isFalse + } + @Test // GH-1947 internal fun inlinesGenericTypesConstructorRules() {