Skip to content

Commit 3faceb0

Browse files
committed
Adapt Kotlin property reference usage through TypedPropertyPaths.
1 parent 42ff423 commit 3faceb0

File tree

6 files changed

+182
-14
lines changed

6 files changed

+182
-14
lines changed

src/main/java/org/springframework/data/core/MemberDescriptor.java

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,26 @@ public ResolvableType getType() {
141141

142142
}
143143

144+
interface KotlinMemberDescriptor extends MemberDescriptor {
145+
146+
KProperty1<?, ?> getKotlinProperty();
147+
148+
}
149+
144150
/**
145151
* Value object describing a Kotlin property in the context of an owning class.
146152
*/
147-
record KPropertyReferenceDescriptor(Class<?> owner, KProperty1<?, ?> property) implements MemberDescriptor {
153+
record KPropertyReferenceDescriptor(Class<?> owner, KProperty1<?, ?> property) implements KotlinMemberDescriptor {
148154

149155
static KPropertyReferenceDescriptor create(Class<?> owner, KProperty1<?, ?> property) {
150156
return new KPropertyReferenceDescriptor(owner, property);
151157
}
152158

159+
@Override
160+
public KProperty1<?, ?> getKotlinProperty() {
161+
return property();
162+
}
163+
153164
@Override
154165
public Class<?> getOwner() {
155166
return owner();
@@ -186,4 +197,54 @@ public ResolvableType getType() {
186197

187198
}
188199

200+
/**
201+
* Value object describing a Kotlin property in the context of an owning class.
202+
*/
203+
record KPropertyPathDescriptor(KPropertyReferenceImpl<?, ?> property) implements KotlinMemberDescriptor {
204+
205+
static KPropertyPathDescriptor create(KPropertyReferenceImpl<?, ?> propertyReference) {
206+
return new KPropertyPathDescriptor(propertyReference);
207+
}
208+
209+
@Override
210+
public KProperty1<?, ?> getKotlinProperty() {
211+
return property();
212+
}
213+
214+
@Override
215+
public Class<?> getOwner() {
216+
return getMember().getDeclaringClass();
217+
}
218+
219+
@Override
220+
public Member getMember() {
221+
222+
Method javaGetter = ReflectJvmMapping.getJavaGetter(property());
223+
if (javaGetter != null) {
224+
return javaGetter;
225+
}
226+
227+
Field javaField = ReflectJvmMapping.getJavaField(property());
228+
229+
if (javaField != null) {
230+
return javaField;
231+
}
232+
233+
throw new IllegalStateException("Cannot resolve member for property '%s'".formatted(property().getName()));
234+
}
235+
236+
@Override
237+
public ResolvableType getType() {
238+
239+
Member member = getMember();
240+
241+
if (member instanceof Method m) {
242+
return ResolvableType.forMethodReturnType(m, getOwner());
243+
}
244+
245+
return ResolvableType.forField((Field) member, getOwner());
246+
}
247+
248+
}
249+
189250
}

src/main/java/org/springframework/data/core/PropertyReferences.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424
import java.util.WeakHashMap;
2525

2626
import org.jspecify.annotations.Nullable;
27+
2728
import org.springframework.beans.BeanUtils;
2829
import org.springframework.core.KotlinDetector;
2930
import org.springframework.core.ResolvableType;
3031
import org.springframework.data.core.MemberDescriptor.FieldDescriptor;
31-
import org.springframework.data.core.MemberDescriptor.KPropertyReferenceDescriptor;
3232
import org.springframework.data.core.MemberDescriptor.MethodDescriptor;
3333
import org.springframework.util.ConcurrentReferenceHashMap;
3434

@@ -96,7 +96,14 @@ private static PropertyReferenceMetadata read(PropertyReference<?, ?> lambda) {
9696

9797
MemberDescriptor reference = reader.read(lambda);
9898

99-
if (KotlinDetector.isKotlinReflectPresent() && reference instanceof KPropertyReferenceDescriptor kProperty) {
99+
if (KotlinDetector.isKotlinReflectPresent()
100+
&& reference instanceof MemberDescriptor.KotlinMemberDescriptor kProperty) {
101+
102+
if (kProperty instanceof MemberDescriptor.KPropertyPathDescriptor) {
103+
throw new IllegalArgumentException("PropertyReference " + kProperty.getKotlinProperty().getName()
104+
+ " is a property path. Use a single property reference.");
105+
}
106+
100107
return KPropertyReferenceMetadata.of(kProperty);
101108
}
102109

@@ -198,8 +205,9 @@ static class KPropertyReferenceMetadata extends PropertyReferenceMetadata {
198205
/**
199206
* Create a new {@code KPropertyReferenceMetadata}.
200207
*/
201-
public static KPropertyReferenceMetadata of(KPropertyReferenceDescriptor descriptor) {
202-
return new KPropertyReferenceMetadata(descriptor.getOwner(), descriptor.property(), descriptor.getType());
208+
public static KPropertyReferenceMetadata of(MemberDescriptor.KotlinMemberDescriptor descriptor) {
209+
return new KPropertyReferenceMetadata(descriptor.getOwner(), descriptor.getKotlinProperty(),
210+
descriptor.getType());
203211
}
204212

205213
public KProperty<?> getProperty() {

src/main/java/org/springframework/data/core/SerializableLambdaReader.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.springframework.core.KotlinDetector;
4949
import org.springframework.core.SpringProperties;
5050
import org.springframework.dao.InvalidDataAccessApiUsageException;
51+
import org.springframework.data.core.MemberDescriptor.KPropertyPathDescriptor;
5152
import org.springframework.data.core.MemberDescriptor.KPropertyReferenceDescriptor;
5253
import org.springframework.util.ClassUtils;
5354
import org.springframework.util.ObjectUtils;
@@ -148,6 +149,11 @@ public MemberDescriptor read(Object lambdaObject) {
148149
&& captured instanceof KProperty1<?, ?> kProperty) {
149150
return new KPropertyReferenceDescriptor(JvmClassMappingKt.getJavaClass(owner), kProperty);
150151
}
152+
153+
if (captured != null //
154+
&& captured instanceof KPropertyReferenceImpl<?, ?> propRef) {
155+
return KPropertyPathDescriptor.create(propRef);
156+
}
151157
}
152158

153159
assertNotConstructor(lambda);

src/main/java/org/springframework/data/core/TypedPropertyPaths.java

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
package org.springframework.data.core;
1717

1818
import kotlin.reflect.KProperty;
19+
import kotlin.reflect.KProperty1;
20+
import kotlin.reflect.jvm.internal.KProperty1Impl;
21+
import kotlin.reflect.jvm.internal.KPropertyImpl;
1922

2023
import java.beans.Introspector;
2124
import java.beans.PropertyDescriptor;
@@ -30,6 +33,7 @@
3033
import java.util.stream.Stream;
3134

3235
import org.jspecify.annotations.Nullable;
36+
3337
import org.springframework.beans.BeanUtils;
3438
import org.springframework.core.KotlinDetector;
3539
import org.springframework.core.ResolvableType;
@@ -95,10 +99,69 @@ public static <T, M, P> TypedPropertyPath<T, P> compose(TypedPropertyPath<T, M>
9599
}
96100

97101
/**
98-
* Create a {@link TypedPropertyPath} from a {@link PropertyReference}.
102+
* Introspect {@link PropertyReference} and return an introspected {@link ResolvedTypedPropertyPath} variant.
99103
*/
104+
@SuppressWarnings({ "unchecked", "rawtypes" })
100105
public static <P, T> TypedPropertyPath<T, P> of(PropertyReference<T, P> lambda) {
101-
return new PropertyReferenceWrapper<>(PropertyReferences.of(lambda));
106+
107+
if (lambda instanceof Resolved) {
108+
return (TypedPropertyPath) lambda;
109+
}
110+
111+
Map<PropertyReference<?, ?>, TypedPropertyPath<?, ?>> cache;
112+
synchronized (resolved) {
113+
cache = (Map) resolved.computeIfAbsent(lambda.getClass().getClassLoader(),
114+
k -> new ConcurrentReferenceHashMap<>());
115+
}
116+
117+
return (TypedPropertyPath) cache.computeIfAbsent(lambda, o -> doResolvePropertyReference(lambda));
118+
}
119+
120+
@SuppressWarnings({ "rawtypes", "unchecked" })
121+
private static <T, P> TypedPropertyPath<?, ?> doResolvePropertyReference(PropertyReference<T, P> lambda) {
122+
123+
if (lambda instanceof PropertyReferences.ResolvedPropertyReferenceSupport resolved) {
124+
return new PropertyReferenceWrapper<>(resolved);
125+
}
126+
127+
PropertyPathMetadata metadata = getMetadata(lambda);
128+
129+
if (KotlinDetector.isKotlinReflectPresent() && metadata instanceof KPropertyPathMetadata kMetadata
130+
&& kMetadata.getProperty() instanceof KPropertyReferenceImpl<?, ?> ref) {
131+
return KotlinDelegate.of(ref);
132+
}
133+
134+
return new ResolvedPropertyReference<>(lambda, metadata);
135+
}
136+
137+
static class KotlinDelegate {
138+
139+
@SuppressWarnings({ "rawtypes", "unchecked" })
140+
public static <T, P> TypedPropertyPath<T, P> of(KProperty1<T, P> property) {
141+
142+
if (property instanceof KPropertyReferenceImpl paths) {
143+
144+
TypedPropertyPath parent = of(paths.getProperty());
145+
TypedPropertyPath child = of(paths.getLeaf());
146+
147+
return TypedPropertyPaths.compose(parent, child);
148+
}
149+
150+
if (property instanceof KPropertyImpl impl) {
151+
152+
Class<?> owner = impl.getJavaField() != null ? impl.getJavaField().getDeclaringClass()
153+
: impl.getGetter().getCaller().getMember().getDeclaringClass();
154+
KPropertyPathMetadata metadata = TypedPropertyPaths.KPropertyPathMetadata
155+
.of(MemberDescriptor.KPropertyReferenceDescriptor.create(owner, property));
156+
return new TypedPropertyPaths.ResolvedKPropertyPath(metadata);
157+
}
158+
159+
if (property.getGetter().getProperty() instanceof KProperty1Impl impl) {
160+
return of(impl);
161+
}
162+
163+
throw new IllegalArgumentException("Property " + property.getName() + " is not a KProperty");
164+
}
102165
}
103166

104167
/**
@@ -117,8 +180,8 @@ public static <P, T> TypedPropertyPath<T, P> of(TypedPropertyPath<T, P> lambda)
117180
k -> new ConcurrentReferenceHashMap<>());
118181
}
119182

120-
return (TypedPropertyPath<T, P>) cache.computeIfAbsent(lambda,
121-
o -> new ResolvedTypedPropertyPath(o, getMetadata(lambda)));
183+
return (TypedPropertyPath) cache.computeIfAbsent(lambda,
184+
o -> new ResolvedTypedPropertyPath(o, doGetMetadata(lambda)));
122185
}
123186

124187
/**
@@ -134,26 +197,44 @@ public static <T, P> TypedPropertyPath<T, P> of(TypedPropertyPath<T, P> delegate
134197
return new ResolvedTypedPropertyPath<>(delegate, metadata);
135198
}
136199

200+
/**
201+
* Retrieve {@link PropertyPathMetadata} for a given {@link PropertyReference}.
202+
*/
203+
public static PropertyPathMetadata getMetadata(PropertyReference<?, ?> lambda) {
204+
return doGetMetadata(lambda);
205+
}
206+
137207
/**
138208
* Retrieve {@link PropertyPathMetadata} for a given {@link TypedPropertyPath}.
139209
*/
140210
public static PropertyPathMetadata getMetadata(TypedPropertyPath<?, ?> lambda) {
211+
return doGetMetadata(lambda);
212+
}
213+
214+
private static PropertyPathMetadata doGetMetadata(Object lambda) {
141215

142216
Map<Object, PropertyPathMetadata> cache;
217+
143218
synchronized (lambdas) {
144219
cache = lambdas.computeIfAbsent(lambda.getClass().getClassLoader(), k -> new ConcurrentReferenceHashMap<>());
145220
}
146-
Map<Object, PropertyPathMetadata> lambdaMap = cache;
147221

148-
return lambdaMap.computeIfAbsent(lambda, o -> read(lambda));
222+
return cache.computeIfAbsent(lambda, o -> read(lambda));
149223
}
150224

151-
private static PropertyPathMetadata read(TypedPropertyPath<?, ?> lambda) {
225+
private static PropertyPathMetadata read(Object lambda) {
152226

153227
MemberDescriptor reference = reader.read(lambda);
154228

155-
if (KotlinDetector.isKotlinReflectPresent() && reference instanceof KPropertyReferenceDescriptor kProperty) {
156-
return KPropertyPathMetadata.of(kProperty);
229+
if (KotlinDetector.isKotlinReflectPresent()) {
230+
231+
if (reference instanceof KPropertyReferenceDescriptor kProperty) {
232+
return KPropertyPathMetadata.of(kProperty);
233+
}
234+
235+
if (reference instanceof MemberDescriptor.KPropertyPathDescriptor kProperty) {
236+
return KPropertyPathMetadata.of(kProperty);
237+
}
157238
}
158239

159240
if (reference instanceof MethodDescriptor method) {
@@ -258,6 +339,13 @@ public static KPropertyPathMetadata of(KPropertyReferenceDescriptor descriptor)
258339
return new KPropertyPathMetadata(descriptor.getOwner(), descriptor.property(), descriptor.getType());
259340
}
260341

342+
/**
343+
* Create a new {@code KPropertyPathMetadata}.
344+
*/
345+
public static KPropertyPathMetadata of(MemberDescriptor.KPropertyPathDescriptor descriptor) {
346+
return new KPropertyPathMetadata(descriptor.getOwner(), descriptor.property(), descriptor.getType());
347+
}
348+
261349
public KProperty<?> getProperty() {
262350
return property;
263351
}

src/test/kotlin/org/springframework/data/core/KPropertyReferenceUnitTests.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class KPropertyReferenceUnitTests {
3737

3838
@Test // GH-3400
3939
fun composedReferenceCreationShouldFail() {
40+
assertThatIllegalArgumentException().isThrownBy { PropertyReference.of(Person::address / Address::city) }
4041
assertThatIllegalArgumentException().isThrownBy { KPropertyReference.of(Person::address / Address::city) }
4142
}
4243

src/test/kotlin/org/springframework/data/core/KTypedPropertyPathUnitTests.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ class KTypedPropertyPathUnitTests {
9191
@Test // GH-3400
9292
fun shouldCreateComposed() {
9393

94+
assertThat(
95+
PropertyPath.of(Person::address / Address::city).toDotPath()
96+
).isEqualTo("address.city")
97+
9498
val path = KTypedPropertyPath.of(Person::address / Address::city)
9599

96100
assertThat(path.toDotPath()).isEqualTo("address.city")

0 commit comments

Comments
 (0)