diff --git a/pom.xml b/pom.xml index 2473b93f59..af241907f1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATACMNS-1197-SNAPSHOT Spring Data Core diff --git a/src/main/java/org/springframework/data/util/ReflectionUtils.java b/src/main/java/org/springframework/data/util/ReflectionUtils.java index 36103ef86d..430661857b 100644 --- a/src/main/java/org/springframework/data/util/ReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/ReflectionUtils.java @@ -15,7 +15,12 @@ */ package org.springframework.data.util; +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KCallable; +import kotlin.reflect.KClass; import kotlin.reflect.KFunction; +import kotlin.reflect.KMutableProperty; +import kotlin.reflect.KProperty; import kotlin.reflect.KType; import kotlin.reflect.jvm.ReflectJvmMapping; import kotlin.reflect.jvm.internal.impl.load.kotlin.header.KotlinClassHeader; @@ -402,11 +407,37 @@ public static boolean isNullable(MethodParameter parameter) { } if (isSupportedKotlinClass(parameter.getDeclaringClass())) { + return KotlinReflectionUtils.isNullable(parameter); + } + + return !parameter.getParameterType().isPrimitive(); + } + + /** + * Reflection utility methods specific to Kotlin reflection. + */ + static class KotlinReflectionUtils { + + /** + * Returns {@literal} whether the given {@link MethodParameter} is nullable. Its declaring method can reference a + * Kotlin function, property or interface property. + * + * @return {@literal true} if {@link MethodParameter} is nullable. + * @since 2.0.1 + */ + static boolean isNullable(MethodParameter parameter) { - KFunction kotlinFunction = ReflectJvmMapping.getKotlinFunction(parameter.getMethod()); + Method method = parameter.getMethod(); + KFunction kotlinFunction = ReflectJvmMapping.getKotlinFunction(method); if (kotlinFunction == null) { - throw new IllegalArgumentException(String.format("Cannot resolve %s to a Kotlin function!", parameter)); + + // Fallback to own lookup because there's no public Kotlin API for that kind of lookup until + // https://youtrack.jetbrains.com/issue/KT-20768 gets resolved. + Optional first = findKFunction(method); + + kotlinFunction = first.orElseThrow( + () -> new IllegalArgumentException(String.format("Cannot resolve %s to a Kotlin function!", parameter))); } KType type = parameter.getParameterIndex() == -1 ? kotlinFunction.getReturnType() @@ -415,6 +446,46 @@ public static boolean isNullable(MethodParameter parameter) { return type.isMarkedNullable(); } - return !parameter.getParameterType().isPrimitive(); + /** + * Lookup a {@link Method} to a {@link KFunction}. + * + * @param method the JVM {@link Method} to look up. + * @return {@link Optional} wrapping a possibly existing {@link KFunction}. + */ + private static Optional findKFunction(Method method) { + + KClass kotlinClass = JvmClassMappingKt.getKotlinClass(method.getDeclaringClass()); + + return kotlinClass.getMembers() // + .stream() // + .flatMap(KotlinReflectionUtils::toKFunctionStream) // + .filter(it -> { + + Method javaMethod = ReflectJvmMapping.getJavaMethod(it); + return javaMethod != null && javaMethod.equals(method); + }) // + .findFirst(); + } + + private static Stream toKFunctionStream(KCallable it) { + + if (it instanceof KMutableProperty) { + + KMutableProperty property = (KMutableProperty) it; + return Stream.of(property.getGetter(), property.getSetter()); + } + + if (it instanceof KProperty) { + + KProperty property = (KProperty) it; + return Stream.of(property.getGetter()); + } + + if (it instanceof KFunction) { + return Stream.of((KFunction) it); + } + + return Stream.empty(); + } } } diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java index dbc27013f6..60a7c7dd13 100755 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java @@ -350,6 +350,14 @@ public void considersRequiredKotlinNullableParameter() { assertThat(repository.findByOptionalId(null)).isNull(); } + @Test // DATACMNS-1197 + public void considersNullabilityForKotlinInterfaceProperties() { + + KotlinUserRepository repository = factory.getRepository(KotlinUserRepository.class); + + assertThatThrownBy(repository::getFindRouteQuery).isInstanceOf(EmptyResultDataAccessException.class); + } + private ConvertingRepository prepareConvertingRepository(final Object expectedValue) { when(factory.queryOne.execute(Mockito.any(Object[].class))).then(invocation -> { diff --git a/src/test/kotlin/org/springframework/data/repository/core/support/KotlinUserRepository.kt b/src/test/kotlin/org/springframework/data/repository/core/support/KotlinUserRepository.kt index d28c8dd6d8..27ef716ba6 100644 --- a/src/test/kotlin/org/springframework/data/repository/core/support/KotlinUserRepository.kt +++ b/src/test/kotlin/org/springframework/data/repository/core/support/KotlinUserRepository.kt @@ -28,4 +28,6 @@ interface KotlinUserRepository : Repository { fun findById(username: String): User fun findByOptionalId(username: String?): User? + + val findRouteQuery: String } \ No newline at end of file