From 39b783e760cc9d91ece042bda6466d540961cf61 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 20 Feb 2025 14:14:23 +0100 Subject: [PATCH] Consider getters using get as getter for boolean Kotlin properties. We now additionally consider get-prefixed methods in addition to is-prefixed methods as getters for boolean properties. Closes #3249 --- .../data/util/KotlinBeanInfoFactory.java | 30 +++++++++----- .../ProxyProjectionFactoryUnitTests.java | 1 + .../data/projection/Entities.kt | 40 +++++++++++++++++++ .../util/KotlinBeanInfoFactoryUnitTests.kt | 35 ++++++++++++++++ 4 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 src/test/kotlin/org/springframework/data/projection/Entities.kt diff --git a/src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java b/src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java index 69d38de146..258268c814 100644 --- a/src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java +++ b/src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java @@ -94,18 +94,9 @@ private static void collectKotlinProperties(Class beanClass, Collection property) { - Method getter = ReflectJvmMapping.getJavaGetter(property); Method setter = property instanceof KMutableProperty kmp ? ReflectJvmMapping.getJavaSetter(kmp) : null; - - if (getter == null) { - Type javaType = ReflectJvmMapping.getJavaType(property.getReturnType()); - getter = ReflectionUtils.findMethod(beanClass, - javaType == Boolean.TYPE ? "is" : "get" + StringUtils.capitalize(property.getName())); - } - - if (getter != null) { - getter = ClassUtils.getMostSpecificMethod(getter, beanClass); - } + Type javaType = ReflectJvmMapping.getJavaType(property.getReturnType()); + Method getter = findGetter(beanClass, property, javaType); if (getter != null && (Modifier.isStatic(getter.getModifiers()) || getter.getParameterCount() != 0)) { continue; @@ -123,6 +114,21 @@ private static void collectKotlinProperties(Class beanClass, Collection beanClass, KProperty property, Type javaType) { + + Method getter = ReflectJvmMapping.getJavaGetter(property); + + if (getter == null && javaType == Boolean.TYPE) { + getter = ReflectionUtils.findMethod(beanClass, "is" + StringUtils.capitalize(property.getName())); + } + + if (getter == null) { + getter = ReflectionUtils.findMethod(beanClass, "get" + StringUtils.capitalize(property.getName())); + } + + return getter != null ? ClassUtils.getMostSpecificMethod(getter, beanClass) : null; + } + private static void collectBasicJavaProperties(Class beanClass, Map descriptors) throws IntrospectionException { @@ -204,5 +210,7 @@ public void setWriteMethod(@Nullable Method writeMethod) { public Method getWriteMethod() { return this.writeMethod; } + } + } diff --git a/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java b/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java index c905845db5..0d8a17d589 100755 --- a/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/projection/ProxyProjectionFactoryUnitTests.java @@ -342,6 +342,7 @@ void supportsNullableWrapperDateToLocalDateTimeConversion() { assertThat(excerpt.getBirthdate()).contains(LocalDateTime.of(1967, 1, 9, 0, 0)); } + interface Contact {} interface CustomerWithLocalDateTime { diff --git a/src/test/kotlin/org/springframework/data/projection/Entities.kt b/src/test/kotlin/org/springframework/data/projection/Entities.kt new file mode 100644 index 0000000000..9b2b89deed --- /dev/null +++ b/src/test/kotlin/org/springframework/data/projection/Entities.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 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.projection + +open class KClassWithJavaGetter() { + + private var fromOuterSpace: Boolean = false + + open fun getFromOuterSpace() = fromOuterSpace + + open fun setFromOuterSpace(newValue: Boolean) { + this.fromOuterSpace = newValue + } + +} + +open class KClassWithIsGetter() { + + private var fromOuterSpace: Boolean = false + + open fun isFromOuterSpace() = fromOuterSpace + + open fun setFromOuterSpace(newValue: Boolean) { + this.fromOuterSpace = newValue + } + +} diff --git a/src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt b/src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt index 1c376029b7..9d97a4d2de 100644 --- a/src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt @@ -58,6 +58,16 @@ class KotlinBeanInfoFactoryUnitTests { assertThat(pds).hasSize(1).extracting("name").contains("value") } + @Test // GH-3249 + internal fun considersBooleanGetAndIsGetters() { + + val isAndGet = BeanUtils.getPropertyDescriptors(KClassWithIsGetter::class.java) + assertThat(isAndGet[0].readMethod.name).isEqualTo("isFromOuterSpace") + + val getOnly = BeanUtils.getPropertyDescriptors(KClassWithGetGetter::class.java) + assertThat(getOnly[0].readMethod.name).isEqualTo("getFromOuterSpace") + } + @Test internal fun determinesInlineClassConsumerProperties() { @@ -200,4 +210,29 @@ class KotlinBeanInfoFactoryUnitTests { class User : AbstractAuditable() { var name: String? = null } + + open class KClassWithGetGetter() { + + private var fromOuterSpace: Boolean = false + + open fun getFromOuterSpace() = fromOuterSpace + + open fun setFromOuterSpace(newValue: Boolean) { + this.fromOuterSpace = newValue + } + } + + open class KClassWithIsGetter() { + + private var fromOuterSpace: Boolean = false + + open fun isFromOuterSpace() = fromOuterSpace + + open fun getFromOuterSpace() = fromOuterSpace + + open fun setFromOuterSpace(newValue: Boolean) { + this.fromOuterSpace = newValue + } + } + }