diff --git a/src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java b/src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java index 050100621..6e4aa59ae 100644 --- a/src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java +++ b/src/main/java/org/springframework/data/util/KotlinBeanInfoFactory.java @@ -29,15 +29,19 @@ import java.beans.PropertyDescriptor; import java.beans.SimpleBeanInfo; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.Arrays; +import java.lang.reflect.Type; import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.LinkedHashMap; +import java.util.Map; import org.springframework.beans.BeanInfoFactory; import org.springframework.beans.BeanUtils; import org.springframework.core.KotlinDetector; import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; /** * {@link BeanInfoFactory} specific to Kotlin types using Kotlin reflection to determine bean properties. @@ -62,7 +66,7 @@ public class KotlinBeanInfoFactory implements BeanInfoFactory, Ordered { KClass kotlinClass = JvmClassMappingKt.getKotlinClass(beanClass); Collection> members = kotlinClass.getMembers(); - Set pds = new LinkedHashSet<>(members.size()); + Map descriptors = new LinkedHashMap<>(members.size(), 1.f); for (KCallable member : members) { @@ -71,6 +75,16 @@ public class KotlinBeanInfoFactory implements BeanInfoFactory, Ordered { 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); + } + if (getter != null && (Modifier.isStatic(getter.getModifiers()) || getter.getParameterCount() != 0)) { continue; } @@ -82,7 +96,7 @@ public class KotlinBeanInfoFactory implements BeanInfoFactory, Ordered { } } - pds.add(new PropertyDescriptor(property.getName(), getter, setter)); + descriptors.put(property.getName(), new PropertyDescriptor(property.getName(), getter, setter)); } } @@ -95,9 +109,17 @@ public class KotlinBeanInfoFactory implements BeanInfoFactory, Ordered { if (javaClass != Object.class) { PropertyDescriptor[] javaPropertyDescriptors = BeanUtils.getPropertyDescriptors(javaClass); - pds.addAll(Arrays.asList(javaPropertyDescriptors)); + + for (PropertyDescriptor descriptor : javaPropertyDescriptors) { + + descriptor = new PropertyDescriptor(descriptor.getName(), specialize(beanClass, descriptor.getReadMethod()), + specialize(beanClass, descriptor.getWriteMethod())); + descriptors.put(descriptor.getName(), descriptor); + } } + PropertyDescriptor[] propertyDescriptors = descriptors.values().toArray(new PropertyDescriptor[0]); + return new SimpleBeanInfo() { @Override public BeanDescriptor getBeanDescriptor() { @@ -106,11 +128,21 @@ public class KotlinBeanInfoFactory implements BeanInfoFactory, Ordered { @Override public PropertyDescriptor[] getPropertyDescriptors() { - return pds.toArray(new PropertyDescriptor[0]); + return propertyDescriptors; } }; } + @Nullable + private static Method specialize(Class beanClass, @Nullable Method method) { + + if (method == null) { + return method; + } + + return ClassUtils.getMostSpecificMethod(method, beanClass); + } + @Override public int getOrder() { return LOWEST_PRECEDENCE - 10; // leave some space for customizations. diff --git a/src/test/java/org/springframework/data/util/Animal.java b/src/test/java/org/springframework/data/util/Animal.java new file mode 100644 index 000000000..69c012adf --- /dev/null +++ b/src/test/java/org/springframework/data/util/Animal.java @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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; + +public class Animal { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt b/src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt index ada3014b6..f02c98a87 100644 --- a/src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/util/KotlinBeanInfoFactoryUnitTests.kt @@ -18,6 +18,8 @@ package org.springframework.data.util import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.springframework.beans.BeanUtils +import org.springframework.data.annotation.Id +import org.springframework.data.domain.Persistable import org.springframework.data.repository.Repository import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport import org.springframework.data.repository.core.support.RepositoryFactorySupport @@ -95,9 +97,11 @@ class KotlinBeanInfoFactoryUnitTests { @Test // GH-2994 internal fun includesPropertiesFromJavaSupertypes() { - val pds = BeanUtils.getPropertyDescriptors(MyRepositoryFactoryBeanImpl::class.java) + val pds = + BeanUtils.getPropertyDescriptors(MyRepositoryFactoryBeanImpl::class.java) - assertThat(pds).extracting("name").contains("myQueryLookupStrategyKey", "repositoryBaseClass") + assertThat(pds).extracting("name") + .contains("myQueryLookupStrategyKey", "repositoryBaseClass") } @Test // GH-2993 @@ -110,6 +114,22 @@ class KotlinBeanInfoFactoryUnitTests { assertThat(pds[0].readMethod).isNotNull() } + @Test // GH-3140 + internal fun specializesBeanMethods() { + + var pds = BeanUtils.getPropertyDescriptors(Entity::class.java) + + assertThat(pds.find { it.name == "id" }!!.readMethod!!.declaringClass).isEqualTo( + Entity::class.java + ) + + pds = BeanUtils.getPropertyDescriptors(DogEntity::class.java) + + assertThat(pds.find { it.name == "name" }!!.readMethod!!.declaringClass).isEqualTo( + DogEntity::class.java + ) + } + data class SimpleDataClass(val id: String, var name: String) @JvmInline @@ -127,7 +147,8 @@ class KotlinBeanInfoFactoryUnitTests { Foo, Bar } - class MyRepositoryFactoryBeanImpl(repository: Class) : RepositoryFactoryBeanSupport(repository) + class MyRepositoryFactoryBeanImpl(repository: Class) : + RepositoryFactoryBeanSupport(repository) where R : Repository, E : Any, I : Any { private var myQueryLookupStrategyKey: String @@ -149,4 +170,23 @@ class KotlinBeanInfoFactoryUnitTests { override var end: Long = -1L protected set } + + class Entity( + private val id: Long? = null, + + val name: String + ) : Persistable { + + override fun getId(): Long? = id + + override fun isNew(): Boolean = id == null + } + + open class DogEntity : Animal() { + + @Id + override fun getName(): String { + return super.getName() + } + } }