Browse Source
We now support type-safe property paths and property references:
Java variants:
PropertyPath.from("name", Person.class) // existing String-based API
PropertyPath.of(Person::getName) // type-safe property reference expression
PropertyPath.from("address.country", Person.class) // existing nested path API
PropertyPath.of(Person::getAddress).then(Address::getCountry) // type-safe composed path expression
PropertyReference.of(Secret::getSecret)
Kotlin variants:
PropertyReference.of(Secret::secret)
PropertyPath.of(Person::address / Address::city)
allowing type-safe usage through e.g.:
Sort.by(Person::getFirstName, Person::getLastName)
Closes: #3400
Original Pull Request: #3409
main
39 changed files with 4779 additions and 88 deletions
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
/* |
||||
* Copyright 2025-present 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; |
||||
|
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode; |
||||
import org.openjdk.jmh.annotations.Fork; |
||||
import org.openjdk.jmh.annotations.Measurement; |
||||
import org.openjdk.jmh.annotations.Mode; |
||||
import org.openjdk.jmh.annotations.OutputTimeUnit; |
||||
import org.openjdk.jmh.annotations.Warmup; |
||||
|
||||
/** |
||||
* Global benchmark settings. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
@Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) |
||||
@Measurement(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) |
||||
@Fork(value = 1, warmups = 0) |
||||
@BenchmarkMode(Mode.Throughput) |
||||
@OutputTimeUnit(TimeUnit.SECONDS) |
||||
public abstract class BenchmarkSettings { |
||||
|
||||
} |
||||
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import org.junit.platform.commons.annotation.Testable; |
||||
import org.openjdk.jmh.annotations.Benchmark; |
||||
|
||||
import org.springframework.data.BenchmarkSettings; |
||||
|
||||
/** |
||||
* Benchmarks for {@link SerializableLambdaReader}. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
@Testable |
||||
public class SerializableLambdaReaderBenchmarks extends BenchmarkSettings { |
||||
|
||||
private static final SerializableLambdaReader reader = new SerializableLambdaReader(PropertyReference.class); |
||||
|
||||
@Benchmark |
||||
public Object benchmarkMethodReference() { |
||||
|
||||
PropertyReference<Person, String> methodReference = Person::firstName; |
||||
return reader.read(methodReference); |
||||
} |
||||
|
||||
@Benchmark |
||||
public Object benchmarkLambda() { |
||||
|
||||
PropertyReference<Person, String> methodReference = person -> person.firstName(); |
||||
return reader.read(methodReference); |
||||
} |
||||
|
||||
record Person(String firstName, String lastName) { |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import org.junit.platform.commons.annotation.Testable; |
||||
import org.openjdk.jmh.annotations.Benchmark; |
||||
import org.springframework.data.BenchmarkSettings; |
||||
|
||||
/** |
||||
* Benchmarks for {@link TypedPropertyPath}. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
@Testable |
||||
public class TypedPropertyPathBenchmarks extends BenchmarkSettings { |
||||
|
||||
@Benchmark |
||||
public Object benchmarkMethodReference() { |
||||
return TypedPropertyPath.path(Person::firstName); |
||||
} |
||||
|
||||
@Benchmark |
||||
public Object benchmarkComposedMethodReference() { |
||||
return TypedPropertyPath.path(Person::address).then(Address::city); |
||||
} |
||||
|
||||
@Benchmark |
||||
public TypedPropertyPath<Person, String> benchmarkLambda() { |
||||
return TypedPropertyPath.path(person -> person.firstName()); |
||||
} |
||||
|
||||
@Benchmark |
||||
public TypedPropertyPath<Person, String> benchmarkComposedLambda() { |
||||
return TypedPropertyPath.path((Person person) -> person.address()).then(address -> address.city()); |
||||
} |
||||
|
||||
@Benchmark |
||||
public Object dotPath() { |
||||
return TypedPropertyPath.path(Person::firstName).toDotPath(); |
||||
} |
||||
|
||||
@Benchmark |
||||
public Object composedDotPath() { |
||||
return TypedPropertyPath.path(Person::address).then(Address::city).toDotPath(); |
||||
} |
||||
|
||||
record Person(String firstName, String lastName, Address address) { |
||||
|
||||
} |
||||
|
||||
record Address(String city) { |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,205 @@
@@ -0,0 +1,205 @@
|
||||
[[property-paths]] |
||||
= Property Paths |
||||
|
||||
This chapter covers the concept of property paths. |
||||
Property paths are a form of navigation through domain classes to apply certain aspects in the context of interacting with the model. |
||||
Application code provides property paths to data access components to express intents such as selection of properties within a query, forming predicates, or applying sorting. |
||||
A property path originates from its owning type and can consist of one to many segments. |
||||
|
||||
[TIP] |
||||
==== |
||||
Following domain-driven design principles the classes that form the backbone of your persistent domain model and that are accessed through Spring Data are called entities. |
||||
An entry point to the object graph is called aggregate root. |
||||
|
||||
Understanding how to navigate and reference these properties is essential for working with repositories and query operations. |
||||
==== |
||||
|
||||
[[property-path-overview]] |
||||
== Property Path Overview |
||||
|
||||
Property paths provide a simple, text-based mechanism to navigate domain model properties. |
||||
This section introduces the fundamentals of property path navigation and demonstrates trade-offs between string-based and type-safe approaches. |
||||
|
||||
.Domain model example |
||||
[tabs] |
||||
====== |
||||
Java:: |
||||
+ |
||||
[source,java,role="primary"] |
||||
---- |
||||
class Person { |
||||
String firstname, lastname; |
||||
int age; |
||||
Address address; |
||||
List<Address> previousAddresses; |
||||
|
||||
String getFirstname() { … } // other property accessors omitted for brevity |
||||
|
||||
} |
||||
|
||||
class Address { |
||||
String city, street; |
||||
|
||||
// accessors omitted for brevity |
||||
|
||||
} |
||||
---- |
||||
|
||||
Kotlin:: |
||||
+ |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
class Person { |
||||
var firstname: String? = null |
||||
var lastname: String? = null |
||||
var age: Int = 0 |
||||
var address: Address? = null |
||||
var previousAddresses: List<Address> = emptyList() |
||||
} |
||||
|
||||
class Address { |
||||
var city: String? = null |
||||
var street: String? = null |
||||
} |
||||
---- |
||||
====== |
||||
|
||||
Property paths use dot-notation to express property references throughout Spring Data operations, such as sorting and filtering: |
||||
|
||||
.Dot-notation property references |
||||
[source,java] |
||||
---- |
||||
Sort.by("firstname", "address.city") |
||||
---- |
||||
|
||||
A property path consists of one or more segments separated by a dot (`.`). |
||||
Methods accepting property paths support single-segment references (top-level properties) and multi-segment navigation unless otherwise indicated. |
||||
|
||||
Collection and array properties support transparent traversal to their component type, enabling direct reference to nested properties: |
||||
|
||||
---- |
||||
Sort.by("address.city") <1> |
||||
|
||||
Sort.by("previousAddresses") <2> |
||||
|
||||
Sort.by("previousAddresses.city") <3> |
||||
---- |
||||
|
||||
<1> Navigate from the top-level `address` property to the `city` field. |
||||
<2> Reference the entire `previousAddresses` collection (supported by certain technologies for collection-based sorting). |
||||
<3> Navigate through the collection to sort by the `city` field of each address. |
||||
|
||||
String-based property paths offer simplicity and can be broadly applied but there are tradeoffs to consider: |
||||
|
||||
* **Flexibility**: Property paths are flexible and can be constructed from constant string, configuration or as result of user input. |
||||
* **Untyped**: String paths do not carry compile-time type information. |
||||
Typed as textual content they do not have a dependency on the underlying domain type. |
||||
* **Refactoring risk**: Renaming domain properties requires often manual updates to string literals; IDEs cannot reliably track these references. |
||||
|
||||
To improve refactoring safety and type consistency, prefer type-safe property references using method references. |
||||
This approach associates property paths with compile-time type information and enables compiler validation and IDE-driven refactoring. |
||||
See <<type-safe-property-references>> for details. |
||||
|
||||
NOTE: For implementation details, refer to <<property-path-internals>> for more information. |
||||
|
||||
[[property-path-internals]] |
||||
=== Property Path Internals |
||||
|
||||
The {spring-data-commons-javadoc-base}/org/springframework/data/core/package-summary.html[`org.springframework.data.core`] package is the basis for Spring Data's navigation across domain classes. |
||||
The javadoc:org.springframework.data.core.TypeInformation[] interface provides type introspection capable of resolving the type of a property. javadoc:org.springframework.data.core.PropertyPath[] represents a textual navigation path through a domain class. |
||||
|
||||
Together they provide: |
||||
|
||||
* Generic type resolution and introspection |
||||
* Property path creation and validation |
||||
* Actual type resolution for complex properties such as collections and maps |
||||
|
||||
[[type-safe-property-references]] |
||||
== Type-safe Property-References |
||||
|
||||
Type-safe property-references eliminate a common source of errors in data access code: Brittle, string-based property references. |
||||
This section explains how method references can be used to express refactoring-safe property paths. |
||||
|
||||
While a property path is a simple representation of object navigation, String-based property paths are inherently fragile during refactoring as they can be easily missed with an increasing distance between the property definition and its usage. |
||||
Type-safe alternatives derive property paths from method references, enabling the compiler to validate property names and IDEs to support refactoring operations. |
||||
|
||||
[tabs] |
||||
====== |
||||
Java:: |
||||
+ |
||||
[source,java,role="primary"] |
||||
---- |
||||
// Inline usage with Sort |
||||
Sort.by(Person::getFirstName, Person::getLastName); |
||||
|
||||
// Composed navigation |
||||
Sort.by(TypedPropertyPath.of(Person::getAddress).then(Address::getCity), |
||||
Person::getLastName); |
||||
---- |
||||
|
||||
Kotlin:: |
||||
+ |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
// Inline usage with Sort |
||||
Sort.by(Person::firstName, Person::lastName) |
||||
|
||||
// Composed navigation |
||||
Sort.by(Person::address / Address::city, Person::lastName) |
||||
---- |
||||
====== |
||||
|
||||
Type-safe property paths integrate seamlessly with query abstractions and criteria builders, enabling declarative query construction without string-based property references. |
||||
|
||||
Adopting type-safe property references aligns with modern Spring development principles. |
||||
Providing declarative, type-safe, and fluent APIs leads to simpler reasoning about data access eliminating an entire category of potential bugs through IDE refactoring support and early feedback on invalid properties by the compiler. |
||||
|
||||
Lambda introspection is cached for efficiency enabling repeatable use. |
||||
The JVM reuses static lambda instances contributing to minimal overhead of one-time parsing. |
||||
|
||||
You can use `TypedPropertyPath` on its own if you are looking for a type-safe variant which benefits from compiler validation and IDE support for cases that do not directly integrate with Spring Data APIs: |
||||
|
||||
[tabs] |
||||
====== |
||||
Java:: |
||||
+ |
||||
[source,java,role="primary"] |
||||
---- |
||||
import static org.springframework.data.core.TypedPropertyPath.path; |
||||
|
||||
// Static import variant |
||||
path(Person::getAddress) |
||||
.then(Address::getCity); |
||||
|
||||
// Fluent composition |
||||
TypedPropertyPath.of(Person::getAddress) |
||||
.then(Address::getCity); |
||||
---- |
||||
|
||||
Kotlin:: |
||||
+ |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
// Kotlin API |
||||
TypedPropertyPath.of(Person::address / Address::city) |
||||
|
||||
// as extension function |
||||
(Person::address / Address::city).toPath() |
||||
---- |
||||
====== |
||||
|
||||
[[type-safe-property-references-recommendations]] |
||||
=== Type-safe Property-Reference API Recommendations |
||||
|
||||
When using (or building) APIs using type-safe property-references, consider the following recommendations: |
||||
|
||||
* **Use method references**: Accept method references (e.g., `Person::getFirstName`) instead of strings to leverage compile-time validation and IDE refactoring support. |
||||
Method references are preferred as they share similary representations with both Java and Kotlin. |
||||
Additionally, method references provide a better performance baseline compared to lambdas due to their simpler representation. |
||||
* **Leverage the `T` type of `TypedPropertyPath`**: Whenever accepting a typed property path, consider using the generic type `T` of `TypedPropertyPath<T, P>`. |
||||
Limiting property paths to a specific domain type that is used within the current operation reduces the potential of using unintended properties from other types. + |
||||
+ |
||||
Whenever accepting or providing multiple property paths, consider using `TypedPropertyPath<T, ?>` to allow for properties within the context of the owning type `T` to limit property paths to a common owning type. |
||||
|
||||
NOTE: When using Graal Native Image compilation, you need to provide reachability metadata for serializable `TypedPropertyPath` lambdas. |
||||
When using lambda expressions instead of method references you will have to include the Java source code of the class containing the lambda expression in the native image configuration. |
||||
@ -0,0 +1,250 @@
@@ -0,0 +1,250 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import kotlin.reflect.KProperty1; |
||||
import kotlin.reflect.jvm.ReflectJvmMapping; |
||||
|
||||
import java.lang.invoke.SerializedLambda; |
||||
import java.lang.reflect.Field; |
||||
import java.lang.reflect.Member; |
||||
import java.lang.reflect.Method; |
||||
|
||||
import org.springframework.asm.Type; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.ReflectionUtils; |
||||
|
||||
/** |
||||
* Interface representing a member reference such as a field or method. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 4.1 |
||||
*/ |
||||
interface MemberDescriptor { |
||||
|
||||
/** |
||||
* @return class owning the member, can be the declaring class or a subclass. |
||||
*/ |
||||
Class<?> getOwner(); |
||||
|
||||
/** |
||||
* @return the member (field or method). |
||||
*/ |
||||
Member getMember(); |
||||
|
||||
/** |
||||
* @return field type or method return type. |
||||
*/ |
||||
ResolvableType getType(); |
||||
|
||||
/** |
||||
* Create {@link MethodDescriptor} from a serialized lambda representing a method reference. |
||||
*/ |
||||
static MethodDescriptor ofMethodReference(ClassLoader classLoader, SerializedLambda lambda) |
||||
throws ClassNotFoundException { |
||||
return ofMethod(classLoader, Type.getObjectType(lambda.getImplClass()).getClassName(), lambda.getImplMethodName()); |
||||
} |
||||
|
||||
/** |
||||
* Create {@link MethodDescriptor} from owner type and method name. |
||||
*/ |
||||
static MethodDescriptor ofMethod(ClassLoader classLoader, String ownerClassName, String name) |
||||
throws ClassNotFoundException { |
||||
Class<?> owner = ClassUtils.forName(ownerClassName, classLoader); |
||||
return MethodDescriptor.create(owner, name); |
||||
} |
||||
|
||||
/** |
||||
* Create {@link MethodDescriptor.FieldDescriptor} from owner type, field name and field type. |
||||
*/ |
||||
static MethodDescriptor.FieldDescriptor ofField(ClassLoader classLoader, String ownerClassName, String name, |
||||
String fieldType) throws ClassNotFoundException { |
||||
|
||||
Class<?> owner = ClassUtils.forName(ownerClassName, classLoader); |
||||
Class<?> type = ClassUtils.forName(fieldType, classLoader); |
||||
|
||||
return FieldDescriptor.create(owner, name, type); |
||||
} |
||||
|
||||
/** |
||||
* Value object describing a {@link Method} in the context of an owning class. |
||||
*/ |
||||
record MethodDescriptor(Class<?> owner, Method method) implements MemberDescriptor { |
||||
|
||||
static MethodDescriptor create(Class<?> owner, String methodName) { |
||||
Method method = ReflectionUtils.findMethod(owner, methodName); |
||||
if (method == null) { |
||||
throw new IllegalArgumentException("Method '%s.%s()' not found".formatted(owner.getName(), methodName)); |
||||
} |
||||
return new MethodDescriptor(owner, method); |
||||
} |
||||
|
||||
@Override |
||||
public Class<?> getOwner() { |
||||
return owner(); |
||||
} |
||||
|
||||
@Override |
||||
public Method getMember() { |
||||
return method(); |
||||
} |
||||
|
||||
@Override |
||||
public ResolvableType getType() { |
||||
return ResolvableType.forMethodReturnType(method(), owner()); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Value object describing a {@link Field} in the context of an owning class. |
||||
*/ |
||||
record FieldDescriptor(Class<?> owner, Field field) implements MemberDescriptor { |
||||
|
||||
static FieldDescriptor create(Class<?> owner, String fieldName, Class<?> fieldType) { |
||||
|
||||
Field field = ReflectionUtils.findField(owner, fieldName, fieldType); |
||||
if (field == null) { |
||||
throw new IllegalArgumentException("Field '%s.%s' not found".formatted(owner.getName(), fieldName)); |
||||
} |
||||
return new FieldDescriptor(owner, field); |
||||
} |
||||
|
||||
@Override |
||||
public Class<?> getOwner() { |
||||
return owner(); |
||||
} |
||||
|
||||
@Override |
||||
public Field getMember() { |
||||
return field(); |
||||
} |
||||
|
||||
@Override |
||||
public ResolvableType getType() { |
||||
return ResolvableType.forField(field(), owner()); |
||||
} |
||||
|
||||
} |
||||
|
||||
interface KotlinMemberDescriptor extends MemberDescriptor { |
||||
|
||||
KProperty1<?, ?> getKotlinProperty(); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Value object describing a Kotlin property in the context of an owning class. |
||||
*/ |
||||
record KPropertyReferenceDescriptor(Class<?> owner, KProperty1<?, ?> property) implements KotlinMemberDescriptor { |
||||
|
||||
static KPropertyReferenceDescriptor create(Class<?> owner, KProperty1<?, ?> property) { |
||||
return new KPropertyReferenceDescriptor(owner, property); |
||||
} |
||||
|
||||
@Override |
||||
public KProperty1<?, ?> getKotlinProperty() { |
||||
return property(); |
||||
} |
||||
|
||||
@Override |
||||
public Class<?> getOwner() { |
||||
return owner(); |
||||
} |
||||
|
||||
@Override |
||||
public Member getMember() { |
||||
|
||||
Method javaGetter = ReflectJvmMapping.getJavaGetter(property()); |
||||
if (javaGetter != null) { |
||||
return javaGetter; |
||||
} |
||||
|
||||
Field javaField = ReflectJvmMapping.getJavaField(property()); |
||||
|
||||
if (javaField != null) { |
||||
return javaField; |
||||
} |
||||
|
||||
throw new IllegalStateException("Cannot resolve member for property '%s'".formatted(property().getName())); |
||||
} |
||||
|
||||
@Override |
||||
public ResolvableType getType() { |
||||
|
||||
Member member = getMember(); |
||||
|
||||
if (member instanceof Method m) { |
||||
return ResolvableType.forMethodReturnType(m, owner()); |
||||
} |
||||
|
||||
return ResolvableType.forField((Field) member, owner()); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Value object describing a Kotlin property in the context of an owning class. |
||||
*/ |
||||
record KPropertyPathDescriptor(KProperty1<?, ?> property) implements KotlinMemberDescriptor { |
||||
|
||||
static KPropertyPathDescriptor create(KProperty1<?, ?> propertyReference) { |
||||
return new KPropertyPathDescriptor(propertyReference); |
||||
} |
||||
|
||||
@Override |
||||
public KProperty1<?, ?> getKotlinProperty() { |
||||
return property(); |
||||
} |
||||
|
||||
@Override |
||||
public Class<?> getOwner() { |
||||
return getMember().getDeclaringClass(); |
||||
} |
||||
|
||||
@Override |
||||
public Member getMember() { |
||||
|
||||
Method javaGetter = ReflectJvmMapping.getJavaGetter(property()); |
||||
if (javaGetter != null) { |
||||
return javaGetter; |
||||
} |
||||
|
||||
Field javaField = ReflectJvmMapping.getJavaField(property()); |
||||
|
||||
if (javaField != null) { |
||||
return javaField; |
||||
} |
||||
|
||||
throw new IllegalStateException("Cannot resolve member for property '%s'".formatted(property().getName())); |
||||
} |
||||
|
||||
@Override |
||||
public ResolvableType getType() { |
||||
|
||||
Member member = getMember(); |
||||
|
||||
if (member instanceof Method m) { |
||||
return ResolvableType.forMethodReturnType(m, getOwner()); |
||||
} |
||||
|
||||
return ResolvableType.forField((Field) member, getOwner()); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,138 @@
@@ -0,0 +1,138 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import java.io.Serializable; |
||||
import java.lang.invoke.SerializedLambda; |
||||
import java.lang.reflect.Method; |
||||
import java.util.Objects; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ReflectionUtils; |
||||
|
||||
/** |
||||
* Utility class for {@link PropertyPath} and {@link PropertyReference} implementations. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 4.1 |
||||
*/ |
||||
public class PropertyPathUtil { |
||||
|
||||
/** |
||||
* Resolve a {@link PropertyPath} from a {@link Serializable} lambda implementing a functional interface accepting a |
||||
* single method argument and returning a value. The form of the interface must follow a design aligned with |
||||
* {@link org.springframework.core.convert.converter.Converter} or {@link java.util.function.Function}. |
||||
* |
||||
* @param obj the serializable lambda object. |
||||
* @return the resolved property path. |
||||
*/ |
||||
public static PropertyPath resolve(Object obj) { |
||||
|
||||
Assert.isInstanceOf(Serializable.class, obj, "Object must be Serializable"); |
||||
|
||||
return TypedPropertyPaths.of(new SerializableWrapper((Serializable) obj)); |
||||
} |
||||
|
||||
private record SerializableWrapper(Serializable serializable) implements PropertyReference<Object, Object> { |
||||
|
||||
@Override |
||||
public @Nullable Object get(Object obj) { |
||||
return null; |
||||
} |
||||
|
||||
// serializable bridge
|
||||
public SerializedLambda writeReplace() { |
||||
|
||||
Method method = ReflectionUtils.findMethod(serializable.getClass(), "writeReplace"); |
||||
|
||||
if (method == null) { |
||||
throw new InvalidDataAccessApiUsageException( |
||||
"Cannot find writeReplace method on " + serializable.getClass().getName()); |
||||
} |
||||
|
||||
ReflectionUtils.makeAccessible(method); |
||||
return (SerializedLambda) ReflectionUtils.invokeMethod(method, serializable); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Compute the hash code for the given {@link PropertyPath} based on its {@link Object#toString() string} |
||||
* representation. |
||||
* |
||||
* @param path the property path. |
||||
* @return property path hash code. |
||||
*/ |
||||
static int hashCode(PropertyPath path) { |
||||
return path.toString().hashCode(); |
||||
} |
||||
|
||||
/** |
||||
* Compute the hash code for the given {@link PropertyReference} based on its {@link Object#toString() string} |
||||
* representation. |
||||
* |
||||
* @param property the property reference |
||||
* @return property reference hash code. |
||||
*/ |
||||
static int hashCode(PropertyReference<?, ?> property) { |
||||
return Objects.hash(property.getOwningType(), property.getName()); |
||||
} |
||||
|
||||
/** |
||||
* Equality check for {@link PropertyPath} implementations based on their owning type and string representation. |
||||
* |
||||
* @param self the property path. |
||||
* @param o the other object. |
||||
* @return {@literal true} if both are equal; {@literal false} otherwise. |
||||
*/ |
||||
static boolean equals(PropertyPath self, @Nullable Object o) { |
||||
|
||||
if (self == o) { |
||||
return true; |
||||
} |
||||
|
||||
if (!(o instanceof PropertyPath that)) { |
||||
return false; |
||||
} |
||||
|
||||
return Objects.equals(self.getOwningType(), that.getOwningType()) |
||||
&& Objects.equals(self.toString(), that.toString()); |
||||
} |
||||
|
||||
/** |
||||
* Equality check for {@link PropertyReference} implementations based on their owning type and name. |
||||
* |
||||
* @param self the property path. |
||||
* @param o the other object. |
||||
* @return {@literal true} if both are equal; {@literal false} otherwise. |
||||
*/ |
||||
static boolean equals(PropertyReference<?, ?> self, @Nullable Object o) { |
||||
|
||||
if (self == o) { |
||||
return true; |
||||
} |
||||
|
||||
if (!(o instanceof PropertyReference<?, ?> that)) { |
||||
return false; |
||||
} |
||||
|
||||
return Objects.equals(self.getOwningType(), that.getOwningType()) && Objects.equals(self.getName(), that.getName()); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,205 @@
@@ -0,0 +1,205 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
/** |
||||
* Interface providing type-safe property references. |
||||
* <p> |
||||
* This functional interface is typically implemented through method references that allow for compile-time type safety |
||||
* and refactoring support. Instead of string-based property names that are easy to miss when changing the domain model, |
||||
* {@code PropertyReference} leverages Java's declarative method references to ensure type-safe property access. |
||||
* <p> |
||||
* Create a typed property reference using the static factory method {@link #property(PropertyReference)} with a method |
||||
* reference, for example: |
||||
* |
||||
* <pre class="code"> |
||||
* PropertyReference.property(Person::getName); |
||||
* </pre> |
||||
* |
||||
* The resulting object can be used to obtain the {@link #getName() property name} and to interact with the target |
||||
* property. Typed references can be used to compose {@link TypedPropertyPath property paths} to navigate nested object |
||||
* structures using {@link #then(PropertyReference)}: |
||||
* |
||||
* <pre class="code"> |
||||
* TypedPropertyPath<Person, String> city = PropertyReference.of(Person::getAddress).then(Address::getCity); |
||||
* </pre> |
||||
* <p> |
||||
* The generic type parameters preserve type information across the property path chain: {@code T} represents the owning |
||||
* type of the current segment (or the root type for composed paths), while {@code P} represents the property value type |
||||
* at this segment. Composition automatically flows type information forward, ensuring that {@code then()} preserves the |
||||
* full chain's type safety. |
||||
* <p> |
||||
* Implement {@code PropertyReference} using method references (strongly recommended) or lambdas that directly access a |
||||
* property getter. Constructor references, method calls with parameters, and complex expressions are not supported and |
||||
* result in {@link org.springframework.dao.InvalidDataAccessApiUsageException}. Unlike method references, introspection |
||||
* of lambda expressions requires bytecode analysis of the declaration site classes and thus depends on their |
||||
* availability at runtime. |
||||
* |
||||
* @param <T> the owning type of this property. |
||||
* @param <P> the property value type. |
||||
* @author Mark Paluch |
||||
* @since 4.1 |
||||
* @see #property(PropertyReference) |
||||
* @see #then(PropertyReference) |
||||
* @see #of(PropertyReference) |
||||
* @see #ofMany(PropertyReference) |
||||
* @see TypedPropertyPath |
||||
* @see java.beans.PropertyDescriptor |
||||
*/ |
||||
@FunctionalInterface |
||||
public interface PropertyReference<T, P extends @Nullable Object> extends Serializable { |
||||
|
||||
/** |
||||
* Syntax sugar to create a {@link PropertyReference} from a method reference to a Java beans property. Suitable for |
||||
* static imports. |
||||
* <p> |
||||
* This method returns a resolved {@link PropertyReference} by introspecting the given method reference. |
||||
* |
||||
* @param property the method reference to a Java beans property. |
||||
* @param <T> owning type. |
||||
* @param <P> property type. |
||||
* @return the typed property reference. |
||||
*/ |
||||
static <T, P extends @Nullable Object> PropertyReference<T, P> property(PropertyReference<T, P> property) { |
||||
return of(property); |
||||
} |
||||
|
||||
/** |
||||
* Syntax sugar to create a {@link PropertyReference} from a method reference to a Java beans property. |
||||
* <p> |
||||
* This method returns a resolved {@link PropertyReference} by introspecting the given method reference. |
||||
* |
||||
* @param property the method reference to a Java beans property. |
||||
* @param <T> owning type. |
||||
* @param <P> property type. |
||||
* @return the typed property reference. |
||||
*/ |
||||
static <T, P extends @Nullable Object> PropertyReference<T, P> of(PropertyReference<T, P> property) { |
||||
return PropertyReferences.of(property); |
||||
} |
||||
|
||||
/** |
||||
* Syntax sugar to create a {@link PropertyReference} from a method reference to a Java beans property. |
||||
* <p> |
||||
* This method returns a resolved {@link PropertyReference} by introspecting the given method reference. Note that |
||||
* {@link #get(Object)} becomes unusable for collection properties as the property type adapted from |
||||
* {@code Iterable <P>} and a single {@code P} cannot represent a collection of items. |
||||
* |
||||
* @param property the method reference to a Java beans property. |
||||
* @param <T> owning type. |
||||
* @param <P> property type. |
||||
* @return the typed property reference. |
||||
*/ |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
static <T, P> PropertyReference<T, P> ofMany(PropertyReference<T, ? extends Iterable<P>> property) { |
||||
return (PropertyReference) PropertyReferences.of(property); |
||||
} |
||||
|
||||
/** |
||||
* Get the property value for the given object. |
||||
* |
||||
* @param obj the object to get the property value from. |
||||
* @return the property value. |
||||
*/ |
||||
@Nullable |
||||
P get(T obj); |
||||
|
||||
/** |
||||
* Returns the owning type of the referenced property. |
||||
* |
||||
* @return the owningType will never be {@literal null}. |
||||
*/ |
||||
default TypeInformation<T> getOwningType() { |
||||
return PropertyReferences.of(this).getOwningType(); |
||||
} |
||||
|
||||
/** |
||||
* Returns the name of the property. |
||||
* |
||||
* @return the current property name. |
||||
*/ |
||||
default String getName() { |
||||
return PropertyReferences.of(this).getName(); |
||||
} |
||||
|
||||
/** |
||||
* Returns the actual type of the property at this segment. Will return the plain resolved type for simple properties, |
||||
* the component type for any {@link Iterable} or the value type of {@link java.util.Map} properties. |
||||
* |
||||
* @return the actual type of the property. |
||||
* @see #getTypeInformation() |
||||
* @see TypeInformation#getRequiredActualType() |
||||
*/ |
||||
default Class<?> getType() { |
||||
return getTypeInformation().getRequiredActualType().getType(); |
||||
} |
||||
|
||||
/** |
||||
* Returns the type information for the property at this segment. |
||||
* |
||||
* @return the type information for the property at this segment. |
||||
*/ |
||||
default TypeInformation<?> getTypeInformation() { |
||||
return PropertyReferences.of(this).getTypeInformation(); |
||||
} |
||||
|
||||
/** |
||||
* Returns whether the property is a collection. |
||||
* |
||||
* @return {@literal true} if the property is a collection. |
||||
* @see #getTypeInformation() |
||||
* @see TypeInformation#isCollectionLike() |
||||
*/ |
||||
default boolean isCollection() { |
||||
return getTypeInformation().isCollectionLike(); |
||||
} |
||||
|
||||
/** |
||||
* Extend the property to a property path by appending the {@code next} path segment and return a new property path |
||||
* instance. |
||||
* |
||||
* @param next the next property path segment as method reference accepting the owner object {@code P} type and |
||||
* returning {@code N} as result of accessing the property. |
||||
* @param <N> the new property value type. |
||||
* @return a new composed {@code TypedPropertyPath}. |
||||
*/ |
||||
default <N extends @Nullable Object> TypedPropertyPath<T, N> then(PropertyReference<P, N> next) { |
||||
return TypedPropertyPaths.compose(this, next); |
||||
} |
||||
|
||||
/** |
||||
* Extend the property to a property path by appending the {@code next} path segment and return a new property path |
||||
* instance. |
||||
* <p> |
||||
* Note that {@link #get(Object)} becomes unusable for collection properties as the property type adapted from |
||||
* {@code Iterable <P>} and a single {@code P} cannot represent a collection of items. |
||||
* |
||||
* @param next the next property path segment as method reference accepting the owner object {@code P} type and |
||||
* returning {@code N} as result of accessing the property. |
||||
* @param <N> the new property value type. |
||||
* @return a new composed {@code TypedPropertyPath}. |
||||
*/ |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
default <N extends @Nullable Object> TypedPropertyPath<T, N> thenMany( |
||||
PropertyReference<P, ? extends Iterable<N>> next) { |
||||
return (TypedPropertyPath) TypedPropertyPaths.compose(this, next); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,320 @@
@@ -0,0 +1,320 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import kotlin.reflect.KProperty; |
||||
|
||||
import java.beans.Introspector; |
||||
import java.beans.PropertyDescriptor; |
||||
import java.util.Map; |
||||
import java.util.WeakHashMap; |
||||
import java.util.function.Supplier; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
import org.springframework.beans.BeanUtils; |
||||
import org.springframework.core.KotlinDetector; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.data.core.MemberDescriptor.FieldDescriptor; |
||||
import org.springframework.data.core.MemberDescriptor.MethodDescriptor; |
||||
import org.springframework.util.ConcurrentReferenceHashMap; |
||||
|
||||
/** |
||||
* Utility class to read metadata and resolve {@link PropertyReference} instances. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 4.1 |
||||
*/ |
||||
class PropertyReferences { |
||||
|
||||
private static final Map<ClassLoader, Map<PropertyReference<?, ?>, ResolvedPropertyReference<?, ?>>> resolved = new WeakHashMap<>(); |
||||
|
||||
private static final SerializableLambdaReader reader = new SerializableLambdaReader(PropertyReference.class, |
||||
TypedPropertyPath.class, TypedPropertyPaths.class, PropertyReferences.class); |
||||
|
||||
/** |
||||
* Introspect {@link PropertyReference} and return an introspected {@link ResolvedPropertyReference} variant. |
||||
*/ |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
public static <P, T> PropertyReference<T, P> of(PropertyReference<T, P> lambda) { |
||||
|
||||
if (lambda instanceof ResolvedPropertyReferenceSupport<?, ?>) { |
||||
return lambda; |
||||
} |
||||
|
||||
Map<PropertyReference<?, ?>, ResolvedPropertyReference<?, ?>> cache; |
||||
synchronized (resolved) { |
||||
cache = resolved.computeIfAbsent(lambda.getClass().getClassLoader(), k -> new ConcurrentReferenceHashMap<>()); |
||||
} |
||||
|
||||
return (PropertyReference<T, P>) cache.computeIfAbsent(lambda, |
||||
o -> new ResolvedPropertyReference(o, read(lambda))); |
||||
} |
||||
|
||||
/** |
||||
* Retrieve {@link PropertyMetadata} for a given {@link PropertyReference}. |
||||
*/ |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
public static <T, P> PropertyReference<T, P> of(PropertyReference<T, P> delegate, |
||||
PropertyMetadata metadata) { |
||||
|
||||
if (KotlinDetector.isKotlinReflectPresent() && metadata instanceof KPropertyMetadata kmp) { |
||||
return new ResolvedKPropertyReference(kmp.getProperty(), metadata); |
||||
} |
||||
|
||||
return new ResolvedPropertyReference<>(delegate, metadata); |
||||
} |
||||
|
||||
private static PropertyMetadata read(PropertyReference<?, ?> lambda) { |
||||
|
||||
MemberDescriptor reference = reader.read(lambda); |
||||
|
||||
if (KotlinDetector.isKotlinReflectPresent() |
||||
&& reference instanceof MemberDescriptor.KotlinMemberDescriptor kProperty) { |
||||
|
||||
if (kProperty instanceof MemberDescriptor.KPropertyPathDescriptor) { |
||||
throw new IllegalArgumentException("PropertyReference " + kProperty.getKotlinProperty().getName() |
||||
+ " is a property path. Use a single property reference."); |
||||
} |
||||
|
||||
return KPropertyMetadata.of(kProperty); |
||||
} |
||||
|
||||
if (reference instanceof MethodDescriptor method) { |
||||
return PropertyMetadata.ofMethod(method); |
||||
} |
||||
|
||||
return PropertyMetadata.ofField((FieldDescriptor) reference); |
||||
} |
||||
|
||||
/** |
||||
* Metadata describing a property reference including its owner type, property type, and name. |
||||
*/ |
||||
static class PropertyMetadata { |
||||
|
||||
private final TypeInformation<?> owner; |
||||
private final String property; |
||||
private final TypeInformation<?> propertyType; |
||||
|
||||
PropertyMetadata(Class<?> owner, String property, ResolvableType propertyType) { |
||||
this(TypeInformation.of(owner), property, TypeInformation.of(propertyType)); |
||||
} |
||||
|
||||
PropertyMetadata(TypeInformation<?> owner, String property, TypeInformation<?> propertyType) { |
||||
this.owner = owner; |
||||
this.property = property; |
||||
this.propertyType = propertyType; |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@code PropertyMetadata} from a method. |
||||
*/ |
||||
public static PropertyMetadata ofMethod(MethodDescriptor descriptor) { |
||||
|
||||
return resolveProperty(descriptor, |
||||
() -> new IllegalArgumentException("Cannot find PropertyDescriptor from method '%s.%s()'" |
||||
.formatted(descriptor.owner().getName(), descriptor.getMember().getName()))); |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@code PropertyMetadata} from a field. |
||||
*/ |
||||
public static PropertyMetadata ofField(FieldDescriptor field) { |
||||
return new PropertyMetadata(field.owner(), field.getMember().getName(), field.getType()); |
||||
} |
||||
|
||||
/** |
||||
* Resolve {@code PropertyMetadata} from a method descriptor by introspecting bean metadata and return metadata if |
||||
* available otherwise throw an exception supplied by {@code exceptionSupplier}. |
||||
* |
||||
* @param method the method descriptor. |
||||
* @param exceptionSupplier supplier for exception to be thrown when property cannot be resolved. |
||||
* @return metadata for the resolved property. |
||||
* @see BeanUtils |
||||
*/ |
||||
public static PropertyMetadata resolveProperty(MethodDescriptor method, |
||||
Supplier<? extends RuntimeException> exceptionSupplier) { |
||||
|
||||
PropertyDescriptor descriptor = BeanUtils.findPropertyForMethod(method.method()); |
||||
String methodName = method.method().getName(); |
||||
|
||||
if (descriptor == null) { |
||||
|
||||
String propertyName = getPropertyName(methodName); |
||||
TypeInformation<?> owner = TypeInformation.of(method.owner()); |
||||
TypeInformation<?> fallback = owner.getProperty(propertyName); |
||||
|
||||
if (fallback != null) { |
||||
return new PropertyMetadata(owner, propertyName, fallback); |
||||
} |
||||
|
||||
throw exceptionSupplier.get(); |
||||
} |
||||
|
||||
return new PropertyMetadata(method.owner(), descriptor.getName(), method.getType()); |
||||
} |
||||
|
||||
private static String getPropertyName(String methodName) { |
||||
|
||||
if (methodName.startsWith("is")) { |
||||
return Introspector.decapitalize(methodName.substring(2)); |
||||
} else if (methodName.startsWith("get")) { |
||||
return Introspector.decapitalize(methodName.substring(3)); |
||||
} |
||||
|
||||
return methodName; |
||||
} |
||||
|
||||
public TypeInformation<?> owner() { |
||||
return owner; |
||||
} |
||||
|
||||
public String property() { |
||||
return property; |
||||
} |
||||
|
||||
public TypeInformation<?> propertyType() { |
||||
return propertyType; |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Kotlin-specific {@link PropertyMetadata} implementation. |
||||
*/ |
||||
static class KPropertyMetadata extends PropertyMetadata { |
||||
|
||||
private final KProperty<?> property; |
||||
|
||||
KPropertyMetadata(Class<?> owner, KProperty<?> property, ResolvableType propertyType) { |
||||
super(owner, property.getName(), propertyType); |
||||
this.property = property; |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@code KPropertyMetadata}. |
||||
*/ |
||||
public static KPropertyMetadata of(MemberDescriptor.KotlinMemberDescriptor descriptor) { |
||||
return new KPropertyMetadata(descriptor.getOwner(), descriptor.getKotlinProperty(), |
||||
descriptor.getType()); |
||||
} |
||||
|
||||
public KProperty<?> getProperty() { |
||||
return property; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* A {@link PropertyReference} implementation that caches resolved metadata to avoid repeated introspection. |
||||
* |
||||
* @param <T> the owning type. |
||||
* @param <P> the property type. |
||||
*/ |
||||
static abstract class ResolvedPropertyReferenceSupport<T, P> implements PropertyReference<T, P> { |
||||
|
||||
private final PropertyMetadata metadata; |
||||
private final String toString; |
||||
|
||||
ResolvedPropertyReferenceSupport(PropertyMetadata metadata) { |
||||
this.metadata = metadata; |
||||
this.toString = metadata.owner().getType().getSimpleName() + "." + getName(); |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public TypeInformation<T> getOwningType() { |
||||
return (TypeInformation<T>) metadata.owner(); |
||||
} |
||||
|
||||
@Override |
||||
public String getName() { |
||||
return metadata.property(); |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public TypeInformation<P> getTypeInformation() { |
||||
return (TypeInformation<P>) metadata.propertyType(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(@Nullable Object obj) { |
||||
return PropertyPathUtil.equals(this, obj); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return PropertyPathUtil.hashCode(this); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return toString; |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* A {@link PropertyReference} implementation that caches resolved metadata to avoid repeated introspection. |
||||
* |
||||
* @param <T> the owning type. |
||||
* @param <P> the property type. |
||||
*/ |
||||
static class ResolvedPropertyReference<T, P> extends ResolvedPropertyReferenceSupport<T, P> { |
||||
|
||||
private final PropertyReference<T, P> function; |
||||
|
||||
ResolvedPropertyReference(PropertyReference<T, P> function, PropertyMetadata metadata) { |
||||
super(metadata); |
||||
this.function = function; |
||||
} |
||||
|
||||
@Override |
||||
public @Nullable P get(T obj) { |
||||
return function.get(obj); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* A Kotlin-based {@link PropertyReference} implementation that caches resolved metadata to avoid repeated |
||||
* introspection. |
||||
* |
||||
* @param <T> the owning type. |
||||
* @param <P> the property type. |
||||
*/ |
||||
static class ResolvedKPropertyReference<T, P> extends ResolvedPropertyReferenceSupport<T, P> { |
||||
|
||||
private final KProperty<P> property; |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
ResolvedKPropertyReference(KPropertyMetadata metadata) { |
||||
this((KProperty<P>) metadata.getProperty(), metadata); |
||||
} |
||||
|
||||
ResolvedKPropertyReference(KProperty<P> property, PropertyMetadata metadata) { |
||||
super(metadata); |
||||
this.property = property; |
||||
} |
||||
|
||||
@Override |
||||
public @Nullable P get(T obj) { |
||||
return property.call(obj); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,569 @@
@@ -0,0 +1,569 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import kotlin.jvm.JvmClassMappingKt; |
||||
import kotlin.jvm.internal.PropertyReference; |
||||
import kotlin.reflect.KClass; |
||||
import kotlin.reflect.KProperty1; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.lang.invoke.MethodHandleInfo; |
||||
import java.lang.invoke.SerializedLambda; |
||||
import java.lang.reflect.Member; |
||||
import java.lang.reflect.Method; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.LinkedHashSet; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
import java.util.function.Function; |
||||
import java.util.regex.Pattern; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import org.jspecify.annotations.Nullable; |
||||
import org.springframework.asm.ClassReader; |
||||
import org.springframework.asm.ClassVisitor; |
||||
import org.springframework.asm.Label; |
||||
import org.springframework.asm.MethodVisitor; |
||||
import org.springframework.asm.Opcodes; |
||||
import org.springframework.asm.SpringAsmInfo; |
||||
import org.springframework.asm.Type; |
||||
import org.springframework.core.KotlinDetector; |
||||
import org.springframework.core.SpringProperties; |
||||
import org.springframework.dao.InvalidDataAccessApiUsageException; |
||||
import org.springframework.data.core.MemberDescriptor.KPropertyPathDescriptor; |
||||
import org.springframework.data.core.MemberDescriptor.KPropertyReferenceDescriptor; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.ObjectUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* Reader to extract references to fields and methods from serializable lambda expressions and method references using |
||||
* lambda serialization and bytecode analysis. Allows introspection of property access patterns expressed through |
||||
* functional interfaces without executing the lambda's behavior. Their declarative nature makes method references in |
||||
* general and a constrained subset of lambda expressions suitable to declare property references in the sense of Java |
||||
* Beans properties. Although lambdas and method references are primarily used in a declarative functional programming |
||||
* models to express behavior, lambda serialization allows for further introspection such as parsing the lambda method |
||||
* bytecode for property or member access information and not taking the functional behavior into account. |
||||
* <p> |
||||
* The actual interface is not constrained by a base type, however the object must: |
||||
* <ul> |
||||
* <li>Implement a functional Java interface</li> |
||||
* <li>Must be the top-level lambda (i.e. not wrapped or a functional composition)</li> |
||||
* <li>Implement Serializable (either through the actual interface or through inference)</li> |
||||
* <li>Declare a single method to be implemented</li> |
||||
* <li>Accept a single method argument and return a value</li> |
||||
* </ul> |
||||
* Ideally, the interface has a similar format to {@link Function}, for example: |
||||
* |
||||
* <pre class="code"> |
||||
* interface XtoYFunction<X, Y> (optional: extends Serializable) { |
||||
* Y <method-name>(X someArgument); |
||||
* } |
||||
* </pre> |
||||
* <p> |
||||
* <strong>Supported patterns</strong> |
||||
* <ul> |
||||
* <li>Method references: {@code Person::getName}</li> |
||||
* <li>Property access lambdas: {@code person -> person.getName()}</li> |
||||
* <li>Field access lambdas: {@code person -> person.name}</li> |
||||
* </ul> |
||||
* <strong>Unsupported patterns</strong> |
||||
* <ul> |
||||
* <li>Constructor references: {@code Person::new}</li> |
||||
* <li>Methods with arguments: {@code person -> person.setAge(25)}</li> |
||||
* <li>Lambda expressions that do more than property access, e.g. {@code person -> { person.setAge(25); return |
||||
* person.getName(); }}</li> |
||||
* <li>Arithmetic operations, arbitrary calls</li> |
||||
* <li>Functional composition: {@code Function.andThen(...)}</li> |
||||
* </ul> |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 4.1 |
||||
*/ |
||||
class SerializableLambdaReader { |
||||
|
||||
/** |
||||
* System property that instructs Spring Data to filter stack traces of exceptions thrown during SAM parsing. |
||||
*/ |
||||
public static final String FILTER_STACK_TRACE = "spring.data.lambda-reader.filter-stacktrace"; |
||||
|
||||
/** |
||||
* System property that instructs Spring Data to include suppressed exceptions during SAM parsing. |
||||
*/ |
||||
public static final String INCLUDE_SUPPRESSED_EXCEPTIONS = "spring.data.lambda-reader.include-suppressed-exceptions"; |
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(SerializableLambdaReader.class); |
||||
private static final boolean filterStackTrace = isEnabled(FILTER_STACK_TRACE, true); |
||||
private static final boolean includeSuppressedExceptions = isEnabled(INCLUDE_SUPPRESSED_EXCEPTIONS, false); |
||||
|
||||
private final List<Class<?>> entryPoints; |
||||
|
||||
SerializableLambdaReader(Class<?>... entryPoints) { |
||||
this.entryPoints = Arrays.asList(entryPoints); |
||||
} |
||||
|
||||
private static boolean isEnabled(String property, boolean defaultValue) { |
||||
|
||||
String value = SpringProperties.getProperty(property); |
||||
return StringUtils.hasText(value) ? Boolean.parseBoolean(value) : defaultValue; |
||||
} |
||||
|
||||
/** |
||||
* Read the given lambda object and extract a reference to a {@link Member} such as a field or method. |
||||
* <p> |
||||
* Ideally used with an interface resembling {@link java.util.function.Function}. |
||||
* |
||||
* @param lambdaObject the actual lambda object, must be {@link java.io.Serializable}. |
||||
* @return the member reference. |
||||
* @throws InvalidDataAccessApiUsageException if the lambda object does not contain a valid property reference or hits |
||||
* any of the mentioned limitations. |
||||
*/ |
||||
public MemberDescriptor read(Object lambdaObject) { |
||||
|
||||
SerializedLambda lambda = serialize(lambdaObject); |
||||
|
||||
if (isKotlinPropertyReference(lambda)) { |
||||
return KotlinDelegate.read(lambda); |
||||
} |
||||
|
||||
assertNotConstructor(lambda); |
||||
|
||||
try { |
||||
|
||||
// method reference
|
||||
if ((lambda.getImplMethodKind() == MethodHandleInfo.REF_invokeVirtual |
||||
|| lambda.getImplMethodKind() == MethodHandleInfo.REF_invokeInterface) |
||||
&& !lambda.getImplMethodName().startsWith("lambda$")) { |
||||
return MemberDescriptor.ofMethodReference(lambdaObject.getClass().getClassLoader(), lambda); |
||||
} |
||||
|
||||
// all other lambda forms
|
||||
if (lambda.getImplMethodKind() == MethodHandleInfo.REF_invokeStatic |
||||
|| lambda.getImplMethodKind() == MethodHandleInfo.REF_invokeVirtual) { |
||||
return getMemberDescriptor(lambdaObject, lambda); |
||||
} |
||||
} catch (ReflectiveOperationException | IOException e) { |
||||
throw new InvalidDataAccessApiUsageException("Cannot extract method or field", e); |
||||
} |
||||
|
||||
throw new InvalidDataAccessApiUsageException("Cannot extract method or field from: " + lambdaObject |
||||
+ ". The given value is not a lambda or method reference."); |
||||
} |
||||
|
||||
private void assertNotConstructor(SerializedLambda lambda) { |
||||
|
||||
if (lambda.getImplMethodKind() == MethodHandleInfo.REF_newInvokeSpecial |
||||
|| lambda.getImplMethodKind() == MethodHandleInfo.REF_invokeSpecial) { |
||||
|
||||
InvalidDataAccessApiUsageException e = new InvalidDataAccessApiUsageException( |
||||
"Method reference must not be a constructor call"); |
||||
|
||||
if (filterStackTrace) { |
||||
e.setStackTrace(filterStackTrace(e.getStackTrace(), null)); |
||||
} |
||||
throw e; |
||||
} |
||||
} |
||||
|
||||
private MemberDescriptor getMemberDescriptor(Object lambdaObject, SerializedLambda lambda) throws IOException { |
||||
|
||||
String implClass = Type.getObjectType(lambda.getImplClass()).getClassName(); |
||||
Type owningType = Type.getArgumentTypes(lambda.getImplMethodSignature())[0]; |
||||
String classFileName = implClass.replace('.', '/') + ".class"; |
||||
InputStream classFile = ClassLoader.getSystemResourceAsStream(classFileName); |
||||
|
||||
if (classFile == null) { |
||||
throw new IllegalStateException("Cannot find class file '%s' for lambda introspection".formatted(classFileName)); |
||||
} |
||||
|
||||
try (classFile) { |
||||
|
||||
ClassReader cr = new ClassReader(classFile); |
||||
LambdaReadingVisitor classVisitor = new LambdaReadingVisitor(lambdaObject.getClass().getClassLoader(), |
||||
lambda.getImplMethodName(), owningType); |
||||
cr.accept(classVisitor, ClassReader.SKIP_FRAMES); |
||||
return classVisitor.getMemberReference(lambda); |
||||
} |
||||
} |
||||
|
||||
private static SerializedLambda serialize(Object lambda) { |
||||
|
||||
try { |
||||
Method method = lambda.getClass().getDeclaredMethod("writeReplace"); |
||||
method.setAccessible(true); |
||||
return (SerializedLambda) method.invoke(lambda); |
||||
} catch (ReflectiveOperationException e) { |
||||
throw new InvalidDataAccessApiUsageException( |
||||
"Not a lambda: " + (lambda instanceof Enum<?> ? lambda.getClass().getName() + "#" + lambda : lambda), e); |
||||
} |
||||
} |
||||
|
||||
private static boolean isKotlinPropertyReference(SerializedLambda lambda) { |
||||
|
||||
return KotlinDetector.isKotlinReflectPresent() //
|
||||
&& lambda.getCapturedArgCount() == 1 //
|
||||
&& lambda.getCapturedArg(0) != null //
|
||||
&& KotlinDetector.isKotlinType(lambda.getCapturedArg(0).getClass()); |
||||
} |
||||
|
||||
/** |
||||
* Delegate to detect and read Kotlin property references. |
||||
* <p> |
||||
* Inner class delays loading of Kotlin classes. |
||||
*/ |
||||
static class KotlinDelegate { |
||||
|
||||
public static MemberDescriptor read(SerializedLambda lambda) { |
||||
|
||||
Object captured = lambda.getCapturedArg(0); |
||||
|
||||
if (captured instanceof PropertyReference propRef //
|
||||
&& propRef.getOwner() instanceof KClass<?> owner //
|
||||
&& captured instanceof KProperty1<?, ?> kProperty) { |
||||
return new KPropertyReferenceDescriptor(JvmClassMappingKt.getJavaClass(owner), kProperty); |
||||
} |
||||
|
||||
if (captured instanceof KPropertyPath<?, ?> propRef) { |
||||
return KPropertyPathDescriptor.create(propRef); |
||||
} |
||||
|
||||
throw new InvalidDataAccessApiUsageException("Cannot extract MemberDescriptor from: " + lambda); |
||||
} |
||||
|
||||
} |
||||
|
||||
class LambdaReadingVisitor extends ClassVisitor { |
||||
|
||||
private final String implMethodName; |
||||
private final LambdaMethodVisitor methodVisitor; |
||||
|
||||
public LambdaReadingVisitor(ClassLoader classLoader, String implMethodName, Type owningType) { |
||||
super(SpringAsmInfo.ASM_VERSION); |
||||
this.implMethodName = implMethodName; |
||||
this.methodVisitor = new LambdaMethodVisitor(classLoader, owningType); |
||||
} |
||||
|
||||
public MemberDescriptor getMemberReference(SerializedLambda lambda) { |
||||
return methodVisitor.resolve(lambda); |
||||
} |
||||
|
||||
@Override |
||||
public @Nullable MethodVisitor visitMethod(int access, String name, String desc, String signature, |
||||
String[] exceptions) { |
||||
return name.equals(implMethodName) ? methodVisitor : null; |
||||
} |
||||
|
||||
} |
||||
|
||||
class LambdaMethodVisitor extends MethodVisitor { |
||||
|
||||
private static final Pattern HEX_PATTERN = Pattern.compile("[0-9a-f]+"); |
||||
|
||||
private static final Set<String> BOXING_TYPES = Set.of(Type.getInternalName(Integer.class), |
||||
Type.getInternalName(Long.class), Type.getInternalName(Short.class), Type.getInternalName(Byte.class), |
||||
Type.getInternalName(Float.class), Type.getInternalName(Double.class), Type.getInternalName(Character.class), |
||||
Type.getInternalName(Boolean.class)); |
||||
|
||||
private static final String BOXING_METHOD = "valueOf"; |
||||
|
||||
private final ClassLoader classLoader; |
||||
private final Type owningType; |
||||
private int line; |
||||
private final List<MemberDescriptor> memberDescriptors = new ArrayList<>(); |
||||
private final Set<ReadingError> errors = new LinkedHashSet<>(); |
||||
|
||||
public LambdaMethodVisitor(ClassLoader classLoader, Type owningType) { |
||||
super(SpringAsmInfo.ASM_VERSION); |
||||
this.classLoader = classLoader; |
||||
this.owningType = owningType; |
||||
} |
||||
|
||||
@Override |
||||
public void visitLineNumber(int line, Label start) { |
||||
this.line = line; |
||||
} |
||||
|
||||
@Override |
||||
public void visitInsn(int opcode) { |
||||
|
||||
// allow primitive and object return
|
||||
if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) { |
||||
return; |
||||
} |
||||
|
||||
// we don't care about stack manipulation
|
||||
if (opcode >= Opcodes.DUP && opcode <= Opcodes.DUP2_X2) { |
||||
return; |
||||
} |
||||
|
||||
// no-op
|
||||
if (opcode == Opcodes.NOP) { |
||||
return; |
||||
} |
||||
|
||||
visitLdcInsn(""); |
||||
} |
||||
|
||||
@Override |
||||
public void visitLdcInsn(Object value) { |
||||
errors.add(new ReadingError(line, |
||||
"Code loads a constant. Only method calls to getters, record components, or field access allowed.", null)); |
||||
} |
||||
|
||||
@Override |
||||
public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { |
||||
|
||||
if (opcode == Opcodes.PUTSTATIC || opcode == Opcodes.PUTFIELD) { |
||||
errors.add(new ReadingError(line, String.format("Code attempts to set field '%s'", name), null)); |
||||
return; |
||||
} |
||||
|
||||
Type fieldType = Type.getType(descriptor); |
||||
|
||||
try { |
||||
this.memberDescriptors |
||||
.add(MemberDescriptor.ofField(classLoader, owningType.getClassName(), name, fieldType.getClassName())); |
||||
} catch (ReflectiveOperationException e) { |
||||
if (LOGGER.isTraceEnabled()) { |
||||
LOGGER.trace("Failed to resolve field '%s.%s'".formatted(owner, name), e); |
||||
} |
||||
errors.add(new ReadingError(line, e.getMessage())); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { |
||||
|
||||
if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) { |
||||
errors.add(new ReadingError(line, "Constructor calls not supported.", null)); |
||||
return; |
||||
} |
||||
|
||||
int count = Type.getArgumentCount(descriptor); |
||||
|
||||
if (count != 0) { |
||||
|
||||
if (BOXING_TYPES.contains(owner) && name.equals(BOXING_METHOD)) { |
||||
return; |
||||
} |
||||
|
||||
errors.add(new ReadingError(line, "Method references must invoke no-arg methods only")); |
||||
return; |
||||
} |
||||
|
||||
Type ownerType = Type.getObjectType(owner); |
||||
if (!ownerType.equals(this.owningType)) { |
||||
|
||||
Type[] argumentTypes = Type.getArgumentTypes(descriptor); |
||||
String signature = Arrays.stream(argumentTypes).map(Type::getClassName).collect(Collectors.joining(", ")); |
||||
errors.add(new ReadingError(line, |
||||
"Cannot derive method reference from '%s#%s(%s)': Method calls allowed on owning type '%s' only." |
||||
.formatted(ownerType.getClassName(), name, signature, this.owningType.getClassName()))); |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
this.memberDescriptors.add(MemberDescriptor.ofMethod(classLoader, owningType.getClassName(), name)); |
||||
} catch (ReflectiveOperationException e) { |
||||
|
||||
if (LOGGER.isTraceEnabled()) { |
||||
LOGGER.trace("Failed to resolve method '%s.%s'".formatted(owner, name), e); |
||||
} |
||||
errors.add(new ReadingError(line, e.getMessage())); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Resolve a {@link MemberDescriptor} from a {@link SerializedLambda}. |
||||
* |
||||
* @param lambda the lambda to introspect. |
||||
* @return the resolved member descriptor. |
||||
*/ |
||||
public MemberDescriptor resolve(SerializedLambda lambda) { |
||||
|
||||
// TODO composite path information
|
||||
if (errors.isEmpty()) { |
||||
|
||||
if (memberDescriptors.isEmpty()) { |
||||
throw new InvalidDataAccessApiUsageException("There is no method or field access"); |
||||
} |
||||
|
||||
return memberDescriptors.get(memberDescriptors.size() - 1); |
||||
} |
||||
|
||||
if (lambda.getImplMethodKind() == MethodHandleInfo.REF_invokeStatic |
||||
|| lambda.getImplMethodKind() == MethodHandleInfo.REF_invokeVirtual) { |
||||
|
||||
String methodName = getDeclaringMethodName(lambda); |
||||
|
||||
InvalidDataAccessApiUsageException e = new InvalidDataAccessApiUsageException( |
||||
"Cannot resolve property path%n%nError%s:%n".formatted(errors.size() > 1 ? "s" : "") + errors.stream() |
||||
.map(ReadingError::message).map(LambdaMethodVisitor::formatMessage).collect(Collectors.joining())); |
||||
|
||||
if (includeSuppressedExceptions) { |
||||
for (ReadingError error : errors) { |
||||
if (error.e != null) { |
||||
e.addSuppressed(error.e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (filterStackTrace) { |
||||
e.setStackTrace( |
||||
filterStackTrace(e.getStackTrace(), userCode -> createSynthetic(lambda, methodName, userCode))); |
||||
} |
||||
|
||||
throw e; |
||||
} |
||||
|
||||
throw new InvalidDataAccessApiUsageException("Error resolving " + errors); |
||||
} |
||||
|
||||
private static String formatMessage(String args) { |
||||
|
||||
String[] split = args.split("%n".formatted()); |
||||
StringBuilder builder = new StringBuilder(); |
||||
|
||||
for (int i = 0; i < split.length; i++) { |
||||
|
||||
if (i == 0) { |
||||
builder.append("\t* %s%n".formatted(split[i])); |
||||
} else { |
||||
builder.append("\t %s%n".formatted(split[i])); |
||||
} |
||||
} |
||||
|
||||
return builder.toString(); |
||||
} |
||||
|
||||
private static String getDeclaringMethodName(SerializedLambda lambda) { |
||||
|
||||
String methodName = lambda.getImplMethodName(); |
||||
if (methodName.startsWith("lambda$")) { |
||||
methodName = methodName.substring("lambda$".length()); |
||||
|
||||
if (methodName.contains("$")) { |
||||
methodName = methodName.substring(0, methodName.lastIndexOf('$')); |
||||
} |
||||
|
||||
if (methodName.contains("$")) { |
||||
String probe = methodName.substring(methodName.lastIndexOf('$') + 1); |
||||
if (HEX_PATTERN.matcher(probe).matches()) { |
||||
methodName = methodName.substring(0, methodName.lastIndexOf('$')); |
||||
} |
||||
} |
||||
} |
||||
return methodName; |
||||
} |
||||
|
||||
private StackTraceElement createSynthetic(SerializedLambda lambda, String methodName, StackTraceElement userCode) { |
||||
|
||||
Type type = Type.getObjectType(lambda.getCapturingClass()); |
||||
|
||||
return new StackTraceElement(null, userCode.getModuleName(), userCode.getModuleVersion(), type.getClassName(), |
||||
methodName, ClassUtils.getShortName(type.getClassName()) + ".java", errors.iterator().next().line()); |
||||
} |
||||
} |
||||
|
||||
private StackTraceElement[] filterStackTrace(StackTraceElement[] stackTrace, |
||||
@Nullable Function<StackTraceElement, StackTraceElement> syntheticSupplier) { |
||||
|
||||
int filterIndex = findEntryPoint(stackTrace); |
||||
|
||||
if (filterIndex != -1) { |
||||
|
||||
int offset = syntheticSupplier == null ? 0 : 1; |
||||
|
||||
StackTraceElement[] copy = new StackTraceElement[(stackTrace.length - filterIndex) + offset]; |
||||
System.arraycopy(stackTrace, filterIndex, copy, offset, stackTrace.length - filterIndex); |
||||
|
||||
if (syntheticSupplier != null) { |
||||
StackTraceElement userCode = copy[1]; |
||||
StackTraceElement synthetic = syntheticSupplier.apply(userCode); |
||||
copy[0] = synthetic; |
||||
} |
||||
return copy; |
||||
} |
||||
|
||||
return stackTrace; |
||||
} |
||||
|
||||
private int findEntryPoint(StackTraceElement[] stackTrace) { |
||||
|
||||
int filterIndex = -1; |
||||
|
||||
for (int i = 0; i < stackTrace.length; i++) { |
||||
|
||||
if (matchesEntrypoint(stackTrace[i].getClassName())) { |
||||
filterIndex = i; |
||||
} |
||||
} |
||||
|
||||
return filterIndex; |
||||
} |
||||
|
||||
private boolean matchesEntrypoint(String className) { |
||||
|
||||
if (className.equals(getClass().getName())) { |
||||
return true; |
||||
} |
||||
|
||||
for (Class<?> entryPoint : entryPoints) { |
||||
if (className.equals(entryPoint.getName())) { |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Value object for reading errors. |
||||
* |
||||
* @param line |
||||
* @param message |
||||
* @param e |
||||
*/ |
||||
record ReadingError(int line, String message, @Nullable Exception e) { |
||||
|
||||
ReadingError(int line, String message) { |
||||
this(line, message, null); |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object o) { |
||||
if (!(o instanceof ReadingError that)) { |
||||
return false; |
||||
} |
||||
if (!ObjectUtils.nullSafeEquals(e, that.e)) { |
||||
return false; |
||||
} |
||||
return ObjectUtils.nullSafeEquals(message, that.message); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return ObjectUtils.nullSafeHash(message, e); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,192 @@
@@ -0,0 +1,192 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import java.io.Serializable; |
||||
import java.util.Collections; |
||||
import java.util.Iterator; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
/** |
||||
* Interface providing type-safe property path navigation through method references expressions. |
||||
* <p> |
||||
* This functional interface extends {@link PropertyPath} to provide compile-time type safety and refactoring support. |
||||
* Instead of using {@link PropertyPath#from(String, TypeInformation) string-based property paths} for textual property |
||||
* representation that are easy to miss when changing the domain model, {@code TypedPropertyPath} leverages Java's |
||||
* declarative method references to ensure type-safe property access. |
||||
* <p> |
||||
* Create a typed property path using the static factory method {@link #of(TypedPropertyPath)} with a method reference , |
||||
* for example: |
||||
* |
||||
* <pre class="code"> |
||||
* TypedPropertyPath.path(Person::getName); |
||||
* </pre> |
||||
* |
||||
* The resulting object can be used to obtain the {@link #toDotPath() dot-path} and to interact with the targeting |
||||
* property. Typed paths allow for composition to navigate nested object structures using |
||||
* {@link #then(PropertyReference)}: |
||||
* |
||||
* <pre class="code"> |
||||
* // factory method chaining
|
||||
* TypedPropertyPath<Person, String> city = TypedPropertyPath.path(Person::getAddress, Address::getCity); |
||||
* |
||||
* // fluent API
|
||||
* TypedPropertyPath<Person, String> city = TypedPropertyPath.of(Person::getAddress).then(Address::getCity); |
||||
* </pre> |
||||
* <p> |
||||
* The generic type parameters preserve type information across the property path chain: {@code T} represents the owning |
||||
* type of the current segment (or the root type for composed paths), while {@code P} represents the property value type |
||||
* at this segment. Composition automatically flows type information forward, ensuring that {@code then()} preserves the |
||||
* full chain's type safety. |
||||
* <p> |
||||
* Implement {@code TypedPropertyPath} using method references (strongly recommended)s that directly access a property |
||||
* getter. Constructor references, method calls with parameters, and complex expressions are not supported and result in |
||||
* {@link org.springframework.dao.InvalidDataAccessApiUsageException}. Unlike method references, introspection of lambda |
||||
* expressions requires bytecode analysis of the declaration site classes and thus depends on their availability at |
||||
* runtime. |
||||
* |
||||
* @param <T> the owning type of this path segment; the root type for composed paths. |
||||
* @param <P> the property value type at this path segment. |
||||
* @author Mark Paluch |
||||
* @since 4.1 |
||||
* @see #path(PropertyReference) |
||||
* @see #of(PropertyReference) |
||||
* @see #ofMany(PropertyReference) |
||||
* @see #then(PropertyReference) |
||||
* @see PropertyReference |
||||
* @see PropertyPath#of(PropertyReference) |
||||
*/ |
||||
@FunctionalInterface |
||||
public interface TypedPropertyPath<T, P extends @Nullable Object> extends PropertyPath, Serializable { |
||||
|
||||
/** |
||||
* Syntax sugar to create a {@link TypedPropertyPath} from a property described as method reference to a Java beans |
||||
* property. Suitable for static imports. |
||||
* <p> |
||||
* This method returns a resolved {@link TypedPropertyPath} by introspecting the given method reference. |
||||
* |
||||
* @param property the method reference to a Java beans property. |
||||
* @param <T> owning type. |
||||
* @param <P> property type. |
||||
* @return the typed property path. |
||||
*/ |
||||
static <T, P extends @Nullable Object> TypedPropertyPath<T, P> path(PropertyReference<T, P> property) { |
||||
return TypedPropertyPaths.of(property); |
||||
} |
||||
|
||||
/** |
||||
* Syntax sugar to create a {@link TypedPropertyPath} from a method reference to a Java beans property. |
||||
* <p> |
||||
* This method returns a resolved {@link TypedPropertyPath} by introspecting the given method reference. |
||||
* |
||||
* @param property the method reference to a Java beans property. |
||||
* @param <T> owning type. |
||||
* @param <P> property type. |
||||
* @return the typed property path. |
||||
*/ |
||||
static <T, P extends @Nullable Object> TypedPropertyPath<T, P> of(TypedPropertyPath<T, P> property) { |
||||
return TypedPropertyPaths.of(property); |
||||
} |
||||
|
||||
/** |
||||
* Syntax sugar to create a {@link TypedPropertyPath} from a method reference to a Java beans collection property. |
||||
* <p> |
||||
* This method returns a resolved {@link TypedPropertyPath} by introspecting the given method reference. |
||||
* <p> |
||||
* Note that {@link #get(Object)} becomes unusable for collection properties as the property type adapted from |
||||
* {@code Iterable <P>} and a single {@code P} cannot represent a collection of items. |
||||
* |
||||
* @param property the method reference to a Java beans collection property. |
||||
* @param <T> owning type. |
||||
* @param <P> property type. |
||||
* @return the typed property path. |
||||
*/ |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
static <T, P> TypedPropertyPath<T, P> ofMany(TypedPropertyPath<T, ? extends Iterable<P>> property) { |
||||
return (TypedPropertyPath) TypedPropertyPaths.of(property); |
||||
} |
||||
|
||||
/** |
||||
* Get the property value for the given object. |
||||
* |
||||
* @param obj the object to get the property value from. |
||||
* @return the property value. |
||||
*/ |
||||
@Nullable |
||||
P get(T obj); |
||||
|
||||
@Override |
||||
default TypeInformation<T> getOwningType() { |
||||
return TypedPropertyPaths.of(this).getOwningType(); |
||||
} |
||||
|
||||
@Override |
||||
default String getSegment() { |
||||
return TypedPropertyPaths.of(this).getSegment(); |
||||
} |
||||
|
||||
@Override |
||||
default TypeInformation<P> getTypeInformation() { |
||||
return TypedPropertyPaths.of(this).getTypeInformation(); |
||||
} |
||||
|
||||
@Override |
||||
@Nullable |
||||
default PropertyPath next() { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
default boolean hasNext() { |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
default Iterator<PropertyPath> iterator() { |
||||
return Collections.singletonList((PropertyPath) this).iterator(); |
||||
} |
||||
|
||||
/** |
||||
* Extend the property path by appending the {@code next} path segment and return a new property path instance. |
||||
* |
||||
* @param next the next property path segment as method reference accepting the owner object {@code P} type and |
||||
* returning {@code N} as result of accessing a property. |
||||
* @param <N> the new property value type. |
||||
* @return a new composed {@code TypedPropertyPath}. |
||||
*/ |
||||
default <N extends @Nullable Object> TypedPropertyPath<T, N> then(PropertyReference<P, N> next) { |
||||
return TypedPropertyPaths.compose(this, next); |
||||
} |
||||
|
||||
/** |
||||
* Extend the property path by appending the {@code next} path segment and return a new property path instance. |
||||
* <p> |
||||
* Note that {@link #get(Object)} becomes unusable for collection properties as the property type adapted from |
||||
* {@code Iterable <P>} and a single {@code P} cannot represent a collection of items. |
||||
* |
||||
* @param next the next property path segment as method reference accepting the owner object {@code P} type and |
||||
* returning {@code N} as result of accessing a property. |
||||
* @param <N> the new property value type. |
||||
* @return a new composed {@code TypedPropertyPath}. |
||||
*/ |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
default <N extends @Nullable Object> TypedPropertyPath<T, N> thenMany( |
||||
PropertyReference<P, ? extends Iterable<N>> next) { |
||||
return (TypedPropertyPath) TypedPropertyPaths.compose(this, PropertyReference.of(next)); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,571 @@
@@ -0,0 +1,571 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import kotlin.reflect.KProperty; |
||||
import kotlin.reflect.KProperty1; |
||||
import kotlin.reflect.jvm.internal.KProperty1Impl; |
||||
import kotlin.reflect.jvm.internal.KPropertyImpl; |
||||
|
||||
import java.io.Serializable; |
||||
import java.util.Collections; |
||||
import java.util.Iterator; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.WeakHashMap; |
||||
import java.util.stream.Collectors; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
import org.springframework.core.KotlinDetector; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.data.core.MemberDescriptor.KPropertyPathDescriptor; |
||||
import org.springframework.data.core.MemberDescriptor.KPropertyReferenceDescriptor; |
||||
import org.springframework.data.core.MemberDescriptor.MethodDescriptor; |
||||
import org.springframework.data.core.PropertyReferences.PropertyMetadata; |
||||
import org.springframework.util.CompositeIterator; |
||||
import org.springframework.util.ConcurrentReferenceHashMap; |
||||
|
||||
/** |
||||
* Utility class to read metadata and resolve {@link TypedPropertyPath} instances. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 4.1 |
||||
*/ |
||||
class TypedPropertyPaths { |
||||
|
||||
private static final Map<ClassLoader, Map<Serializable, ResolvedTypedPropertyPath<?, ?>>> resolved = new WeakHashMap<>(); |
||||
|
||||
private static final SerializableLambdaReader reader = new SerializableLambdaReader(PropertyPath.class, |
||||
PropertyReference.class, PropertyReferences.class, TypedPropertyPath.class, TypedPropertyPaths.class); |
||||
|
||||
/** |
||||
* Compose a {@link TypedPropertyPath} by appending {@code next}. |
||||
*/ |
||||
public static <T, M, P> TypedPropertyPath<T, P> compose(PropertyReference<T, M> owner, PropertyReference<M, P> next) { |
||||
return compose(of(owner), of(next)); |
||||
} |
||||
|
||||
/** |
||||
* Compose a {@link TypedPropertyPath} by appending {@code next}. |
||||
*/ |
||||
public static <T, M, P> TypedPropertyPath<T, P> compose(TypedPropertyPath<T, M> owner, PropertyReference<M, P> next) { |
||||
return compose(of(owner), of(next)); |
||||
} |
||||
|
||||
/** |
||||
* Compose a {@link TypedPropertyPath} by appending {@code next}. |
||||
*/ |
||||
@SuppressWarnings({ "rawtypes", "unchecked" }) |
||||
public static <T, M, P> TypedPropertyPath<T, P> compose(TypedPropertyPath<T, M> owner, TypedPropertyPath<M, P> next) { |
||||
|
||||
if (owner instanceof ForwardingPropertyPath<?, ?, ?> fwd) { |
||||
|
||||
List<PropertyPath> paths = fwd.stream().map(ForwardingPropertyPath::getSelf).collect(Collectors.toList()); |
||||
Collections.reverse(paths); |
||||
|
||||
ForwardingPropertyPath result = null; |
||||
for (PropertyPath path : paths) { |
||||
|
||||
if (result == null) { |
||||
result = new ForwardingPropertyPath((TypedPropertyPath) path, next); |
||||
} else { |
||||
result = new ForwardingPropertyPath((TypedPropertyPath) path, result); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
return new ForwardingPropertyPath<>(of(owner), next); |
||||
} |
||||
|
||||
/** |
||||
* Introspect {@link PropertyReference} and return an introspected {@link ResolvedTypedPropertyPath} variant. |
||||
*/ |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
public static <P, T> TypedPropertyPath<T, P> of(PropertyReference<T, P> lambda) { |
||||
|
||||
if (lambda instanceof Resolved) { |
||||
return (TypedPropertyPath) lambda; |
||||
} |
||||
|
||||
Map<PropertyReference<?, ?>, TypedPropertyPath<?, ?>> cache; |
||||
synchronized (resolved) { |
||||
cache = (Map) resolved.computeIfAbsent(lambda.getClass().getClassLoader(), |
||||
k -> new ConcurrentReferenceHashMap<>()); |
||||
} |
||||
|
||||
return (TypedPropertyPath) cache.computeIfAbsent(lambda, TypedPropertyPaths::doResolvePropertyReference); |
||||
} |
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" }) |
||||
private static <T, P> TypedPropertyPath<?, ?> doResolvePropertyReference(PropertyReference<T, P> lambda) { |
||||
|
||||
if (lambda instanceof PropertyReferences.ResolvedPropertyReferenceSupport resolved) { |
||||
return new PropertyReferenceWrapper<>(resolved); |
||||
} |
||||
|
||||
PropertyMetadata metadata = read(lambda); |
||||
|
||||
if (KotlinDetector.isKotlinReflectPresent()) { |
||||
if (metadata instanceof KPropertyPathMetadata kMetadata |
||||
&& kMetadata.getProperty() instanceof KPropertyPath<?, ?> ref) { |
||||
return KotlinDelegate.of(ref); |
||||
} |
||||
} |
||||
|
||||
return new ResolvedPropertyReference<>(lambda, metadata); |
||||
} |
||||
|
||||
/** |
||||
* Introspect {@link TypedPropertyPath} and return an introspected {@link ResolvedTypedPropertyPath} variant. |
||||
*/ |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
public static <P, T> TypedPropertyPath<T, P> of(TypedPropertyPath<T, P> lambda) { |
||||
|
||||
if (lambda instanceof Resolved) { |
||||
return lambda; |
||||
} |
||||
|
||||
Map<TypedPropertyPath<?, ?>, TypedPropertyPath<?, ?>> cache; |
||||
synchronized (resolved) { |
||||
cache = (Map) resolved.computeIfAbsent(lambda.getClass().getClassLoader(), |
||||
k -> new ConcurrentReferenceHashMap<>()); |
||||
} |
||||
|
||||
return (TypedPropertyPath) cache.computeIfAbsent(lambda, |
||||
TypedPropertyPaths::doResolvePropertyPathReference); |
||||
} |
||||
|
||||
private static <T, P> TypedPropertyPath<?, ?> doResolvePropertyPathReference(TypedPropertyPath<T, P> lambda) { |
||||
|
||||
PropertyMetadata metadata = read(lambda); |
||||
|
||||
if (KotlinDetector.isKotlinReflectPresent()) { |
||||
if (metadata instanceof KPropertyPathMetadata kMetadata |
||||
&& kMetadata.getProperty() instanceof KPropertyPath<?, ?> ref) { |
||||
return KotlinDelegate.of(ref); |
||||
} |
||||
} |
||||
|
||||
return new ResolvedTypedPropertyPath<>(lambda, metadata); |
||||
} |
||||
|
||||
private static PropertyMetadata read(Object lambda) { |
||||
|
||||
MemberDescriptor reference = reader.read(lambda); |
||||
|
||||
if (KotlinDetector.isKotlinReflectPresent()) { |
||||
|
||||
if (reference instanceof KPropertyReferenceDescriptor descriptor) { |
||||
return KPropertyPathMetadata.of(descriptor); |
||||
} |
||||
|
||||
if (reference instanceof KPropertyPathDescriptor descriptor) { |
||||
return KPropertyPathMetadata.of(descriptor); |
||||
} |
||||
} |
||||
|
||||
if (reference instanceof MethodDescriptor method) { |
||||
return PropertyMetadata.ofMethod(method); |
||||
} |
||||
|
||||
return PropertyMetadata.ofField((MemberDescriptor.MethodDescriptor.FieldDescriptor) reference); |
||||
} |
||||
|
||||
/** |
||||
* Kotlin-specific {@link PropertyMetadata} implementation supporting composed {@link KProperty property paths}. |
||||
*/ |
||||
static class KPropertyPathMetadata extends PropertyMetadata { |
||||
|
||||
private final KProperty<?> property; |
||||
|
||||
KPropertyPathMetadata(Class<?> owner, KProperty<?> property, ResolvableType propertyType) { |
||||
super(owner, property.getName(), propertyType); |
||||
this.property = property; |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@code KPropertyPathMetadata}. |
||||
*/ |
||||
public static KPropertyPathMetadata of(KPropertyReferenceDescriptor descriptor) { |
||||
return new KPropertyPathMetadata(descriptor.getOwner(), descriptor.property(), descriptor.getType()); |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@code KPropertyPathMetadata}. |
||||
*/ |
||||
public static KPropertyPathMetadata of(KPropertyPathDescriptor descriptor) { |
||||
return new KPropertyPathMetadata(descriptor.getOwner(), descriptor.property(), descriptor.getType()); |
||||
} |
||||
|
||||
public KProperty<?> getProperty() { |
||||
return property; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Delegate to handle property path composition of single-property and property-path KProperty1 references. |
||||
*/ |
||||
static class KotlinDelegate { |
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" }) |
||||
public static <T, P> TypedPropertyPath<T, P> of(Object property) { |
||||
|
||||
if (property instanceof KPropertyPath paths) { |
||||
|
||||
TypedPropertyPath parent = of(paths.getProperty()); |
||||
TypedPropertyPath child = of(paths.getLeaf()); |
||||
|
||||
return TypedPropertyPaths.compose(parent, child); |
||||
} |
||||
|
||||
if (property instanceof KPropertyImpl impl) { |
||||
|
||||
Class<?> owner = impl.getJavaField() != null ? impl.getJavaField().getDeclaringClass() |
||||
: impl.getGetter().getCaller().getMember().getDeclaringClass(); |
||||
KPropertyPathMetadata metadata = TypedPropertyPaths.KPropertyPathMetadata |
||||
.of(MemberDescriptor.KPropertyReferenceDescriptor.create(owner, (KProperty1) impl)); |
||||
return new TypedPropertyPaths.ResolvedKPropertyPath(metadata); |
||||
} |
||||
|
||||
if (property instanceof KProperty1 kProperty) { |
||||
|
||||
if (kProperty.getGetter().getProperty() instanceof KProperty1Impl impl) { |
||||
return of(impl); |
||||
} |
||||
|
||||
throw new IllegalArgumentException("Property " + kProperty.getName() + " is not a KProperty"); |
||||
} |
||||
|
||||
throw new IllegalArgumentException("Property " + property + " is not a KProperty"); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Marker interface to indicate a resolved and processed property path. |
||||
*/ |
||||
interface Resolved { |
||||
|
||||
} |
||||
|
||||
/** |
||||
* A {@link TypedPropertyPath} implementation that caches resolved metadata to avoid repeated introspection. |
||||
* |
||||
* @param <T> the owning type. |
||||
* @param <P> the property type. |
||||
*/ |
||||
static abstract class ResolvedTypedPropertyPathSupport<T, P> implements TypedPropertyPath<T, P>, Resolved { |
||||
|
||||
private final PropertyMetadata metadata; |
||||
private final List<PropertyPath> list; |
||||
private final String toString; |
||||
|
||||
ResolvedTypedPropertyPathSupport(PropertyMetadata metadata) { |
||||
this.metadata = metadata; |
||||
this.list = List.of(this); |
||||
this.toString = metadata.owner().getType().getSimpleName() + "." + toDotPath(); |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public TypeInformation<T> getOwningType() { |
||||
return (TypeInformation<T>) metadata.owner(); |
||||
} |
||||
|
||||
@Override |
||||
public String getSegment() { |
||||
return metadata.property(); |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public TypeInformation<P> getTypeInformation() { |
||||
return (TypeInformation<P>) metadata.propertyType(); |
||||
} |
||||
|
||||
@Override |
||||
public Iterator<PropertyPath> iterator() { |
||||
return list.iterator(); |
||||
} |
||||
|
||||
@Override |
||||
public Stream<PropertyPath> stream() { |
||||
return list.stream(); |
||||
} |
||||
|
||||
@Override |
||||
public List<PropertyPath> toList() { |
||||
return list; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(@Nullable Object obj) { |
||||
return PropertyPathUtil.equals(this, obj); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return PropertyPathUtil.hashCode(this); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return toString; |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Wrapper for {@link PropertyReference}. |
||||
* |
||||
* @param <T> the owning type. |
||||
* @param <P> the property type. |
||||
*/ |
||||
static class PropertyReferenceWrapper<T, P> implements TypedPropertyPath<T, P>, Resolved { |
||||
|
||||
private final PropertyReference<T, P> property; |
||||
private final List<PropertyPath> self; |
||||
|
||||
public PropertyReferenceWrapper(PropertyReference<T, P> property) { |
||||
this.property = property; |
||||
this.self = List.of(this); |
||||
} |
||||
|
||||
@Override |
||||
public @Nullable P get(T obj) { |
||||
return property.get(obj); |
||||
} |
||||
|
||||
@Override |
||||
public TypeInformation<T> getOwningType() { |
||||
return property.getOwningType(); |
||||
} |
||||
|
||||
@Override |
||||
public String getSegment() { |
||||
return property.getName(); |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public TypeInformation<P> getTypeInformation() { |
||||
return (TypeInformation<P>) property.getTypeInformation(); |
||||
} |
||||
|
||||
@Override |
||||
public Iterator<PropertyPath> iterator() { |
||||
return self.iterator(); |
||||
} |
||||
|
||||
@Override |
||||
public Stream<PropertyPath> stream() { |
||||
return self.stream(); |
||||
} |
||||
|
||||
@Override |
||||
public List<PropertyPath> toList() { |
||||
return self; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(@Nullable Object obj) { |
||||
return PropertyPathUtil.equals(this, obj); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return PropertyPathUtil.hashCode(this); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return property.toString(); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* A {@link TypedPropertyPath} implementation that caches resolved metadata to avoid repeated introspection. |
||||
* |
||||
* @param <T> the owning type. |
||||
* @param <P> the property type. |
||||
*/ |
||||
static class ResolvedPropertyReference<T, P> extends ResolvedTypedPropertyPathSupport<T, P> { |
||||
|
||||
private final PropertyReference<T, P> function; |
||||
|
||||
ResolvedPropertyReference(PropertyReference<T, P> function, PropertyMetadata metadata) { |
||||
super(metadata); |
||||
this.function = function; |
||||
} |
||||
|
||||
@Override |
||||
public @Nullable P get(T obj) { |
||||
return function.get(obj); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* A {@link TypedPropertyPath} implementation that caches resolved metadata to avoid repeated introspection. |
||||
* |
||||
* @param <T> the owning type. |
||||
* @param <P> the property type. |
||||
*/ |
||||
static class ResolvedTypedPropertyPath<T, P> extends ResolvedTypedPropertyPathSupport<T, P> { |
||||
|
||||
private final TypedPropertyPath<T, P> function; |
||||
|
||||
ResolvedTypedPropertyPath(TypedPropertyPath<T, P> function, PropertyMetadata metadata) { |
||||
super(metadata); |
||||
this.function = function; |
||||
} |
||||
|
||||
@Override |
||||
public @Nullable P get(T obj) { |
||||
return function.get(obj); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* A Kotlin-based {@link TypedPropertyPath} implementation that caches resolved metadata to avoid repeated |
||||
* introspection. |
||||
* |
||||
* @param <T> the owning type. |
||||
* @param <P> the property type. |
||||
*/ |
||||
static class ResolvedKPropertyPath<T, P> extends ResolvedTypedPropertyPathSupport<T, P> { |
||||
|
||||
private final KProperty<P> property; |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
ResolvedKPropertyPath(KPropertyPathMetadata metadata) { |
||||
this((KProperty<P>) metadata.getProperty(), metadata); |
||||
} |
||||
|
||||
ResolvedKPropertyPath(KProperty<P> property, PropertyMetadata metadata) { |
||||
super(metadata); |
||||
this.property = property; |
||||
} |
||||
|
||||
@Override |
||||
public @Nullable P get(T obj) { |
||||
return property.call(obj); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Forwarding implementation to compose a linked {@link TypedPropertyPath} graph. |
||||
* |
||||
* @param self |
||||
* @param nextSegment |
||||
* @param leaf cached leaf property. |
||||
* @param toStringRepresentation cached toString representation. |
||||
*/ |
||||
record ForwardingPropertyPath<T, M, P>(TypedPropertyPath<T, M> self, TypedPropertyPath<M, P> nextSegment, |
||||
PropertyPath leaf, String dotPath, String toStringRepresentation) implements TypedPropertyPath<T, P>, Resolved { |
||||
|
||||
public ForwardingPropertyPath(TypedPropertyPath<T, M> self, TypedPropertyPath<M, P> nextSegment) { |
||||
this(self, nextSegment, nextSegment.getLeafProperty(), getDotPath(self, nextSegment), |
||||
getToString(self, nextSegment)); |
||||
} |
||||
|
||||
private static String getToString(PropertyPath self, PropertyPath nextSegment) { |
||||
return self.getOwningType().getType().getSimpleName() + "." + getDotPath(self, nextSegment); |
||||
} |
||||
|
||||
private static String getDotPath(PropertyPath self, PropertyPath nextSegment) { |
||||
return self.getSegment() + "." + nextSegment.toDotPath(); |
||||
} |
||||
|
||||
public static PropertyPath getSelf(PropertyPath path) { |
||||
return path instanceof ForwardingPropertyPath<?, ?, ?> fwd ? fwd.self() : path; |
||||
} |
||||
|
||||
@Override |
||||
public @Nullable P get(T obj) { |
||||
M intermediate = self.get(obj); |
||||
return intermediate != null ? nextSegment.get(intermediate) : null; |
||||
} |
||||
|
||||
@Override |
||||
public TypeInformation<T> getOwningType() { |
||||
return self.getOwningType(); |
||||
} |
||||
|
||||
@Override |
||||
public String getSegment() { |
||||
return self.getSegment(); |
||||
} |
||||
|
||||
@Override |
||||
public PropertyPath getLeafProperty() { |
||||
return leaf; |
||||
} |
||||
|
||||
@Override |
||||
public String toDotPath() { |
||||
return self.getSegment() + "." + nextSegment.toDotPath(); |
||||
} |
||||
|
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public TypeInformation<P> getTypeInformation() { |
||||
return (TypeInformation<P>) self.getTypeInformation(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean hasNext() { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public PropertyPath next() { |
||||
return nextSegment; |
||||
} |
||||
|
||||
@Override |
||||
public Iterator<PropertyPath> iterator() { |
||||
|
||||
CompositeIterator<PropertyPath> iterator = new CompositeIterator<>(); |
||||
iterator.add(List.of((PropertyPath) this).iterator()); |
||||
iterator.add(nextSegment.iterator()); |
||||
return iterator; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(@Nullable Object o) { |
||||
return PropertyPathUtil.equals(this, o); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return PropertyPathUtil.hashCode(this); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return toStringRepresentation; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
/* |
||||
* Copyright 2018-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.core |
||||
|
||||
import kotlin.reflect.KProperty |
||||
import kotlin.reflect.KProperty1 |
||||
|
||||
|
||||
/** |
||||
* Extension for [KProperty] providing an `toPath` function to render a [KProperty] in dot notation. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 2.5 |
||||
* @see org.springframework.data.core.PropertyPath.toDotPath |
||||
*/ |
||||
fun KProperty<*>.toDotPath(): String = asString(this) |
||||
|
||||
/** |
||||
* Extension for [KProperty1] providing an `toPropertyPath` function to create a [TypedPropertyPath]. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 4.1 |
||||
* @see org.springframework.data.core.PropertyPath.toDotPath |
||||
*/ |
||||
fun <T : Any, P> KProperty1<T, P>.toPropertyPath(): TypedPropertyPath<T, P> = |
||||
KTypedPropertyPath.of(this) |
||||
|
||||
/** |
||||
* Builds [KPropertyPath] from Property References. |
||||
* Refer to a nested property in an embeddable or association. |
||||
* |
||||
* For example, referring to the field "author.name": |
||||
* ``` |
||||
* Book::author / Author::name isEqualTo "Herman Melville" |
||||
* ``` |
||||
* @author Tjeu Kayim |
||||
* @author Yoann de Martino |
||||
* @since 4.1 |
||||
*/ |
||||
@JvmName("div") |
||||
@Suppress("UNCHECKED_CAST") |
||||
operator fun <T, M, P> KProperty1<T, M?>.div(other: KProperty1<M, P?>): KProperty1<T, P> = |
||||
KSinglePropertyReference(this, other) as KProperty1<T, P> |
||||
|
||||
/** |
||||
* Builds [KPropertyPath] from Property References. |
||||
* Refer to a nested property in an embeddable or association. |
||||
* |
||||
* Note, that this function is different from [div] above in the |
||||
* way that it represents a division operator overloading for |
||||
* child references, where parent to child reference relation is 1-M, not 1-1. |
||||
* It implies that parent defines a [Collection] of children. |
||||
** |
||||
* For example, referring to the field "books.title": |
||||
* ``` |
||||
* Author::books / Book::title contains "Bartleby" |
||||
* ``` |
||||
* @author Mikhail Polivakha |
||||
* @since 4.1 |
||||
*/ |
||||
@JvmName("divIterable") |
||||
@Suppress("UNCHECKED_CAST") |
||||
operator fun <T, M, P> KProperty1<T, Collection<M?>?>.div(other: KProperty1<M, P?>): KProperty1<T, P> = |
||||
KIterablePropertyReference(this, other) as KProperty1<T, P> |
||||
@ -0,0 +1,116 @@
@@ -0,0 +1,116 @@
|
||||
/* |
||||
* Copyright 2018-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. |
||||
*/ |
||||
@file:Suppress("UNCHECKED_CAST") |
||||
|
||||
package org.springframework.data.core |
||||
|
||||
import kotlin.reflect.KProperty |
||||
import kotlin.reflect.KProperty1 |
||||
|
||||
/** |
||||
* Abstraction of a property path consisting of [KProperty1]. |
||||
* |
||||
* @author Tjeu Kayim |
||||
* @author Mark Paluch |
||||
* @author Yoann de Martino |
||||
* @since 4.1 |
||||
*/ |
||||
internal interface KPropertyPath<T, out P> : KProperty1<T, P> { |
||||
val property: KProperty1<T, *> |
||||
val leaf: KProperty1<*, P> |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Abstraction of a single property reference wrapping [KProperty1]. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 4.1 |
||||
*/ |
||||
internal class KSinglePropertyReference<T, M, out P>( |
||||
val parent: KProperty1<T, M?>, |
||||
val child: KProperty1<M, P> |
||||
) : KProperty1<T, P> by child as KProperty1<T, P>, KPropertyPath<T, P> { |
||||
|
||||
override fun get(receiver: T): P { |
||||
|
||||
val get = parent.get(receiver) |
||||
|
||||
if (get != null) { |
||||
return child.get(get) |
||||
} |
||||
|
||||
throw NullPointerException("Parent property returned null") |
||||
} |
||||
|
||||
override fun getDelegate(receiver: T): Any { |
||||
return child |
||||
} |
||||
|
||||
override val property: KProperty1<T, *> |
||||
get() = parent |
||||
override val leaf: KProperty1<*, P> |
||||
get() = child |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Abstraction of a property path that consists of parent [KProperty], |
||||
* and child property [KProperty], where parent [parent] has an [Iterable] |
||||
* of children, so it represents 1-M mapping. |
||||
* |
||||
* @author Mikhail Polivakha |
||||
* @since 4.1 |
||||
*/ |
||||
internal class KIterablePropertyReference<T, M, out P>( |
||||
val parent: KProperty1<T, Iterable<M?>?>, |
||||
val child: KProperty1<M, P> |
||||
) : KProperty1<T, P> by child as KProperty1<T, P>, KPropertyPath<T, P> { |
||||
|
||||
override fun get(receiver: T): P { |
||||
throw UnsupportedOperationException("Collection retrieval not supported") |
||||
} |
||||
|
||||
override fun getDelegate(receiver: T): Any { |
||||
throw UnsupportedOperationException("Collection retrieval not supported") |
||||
} |
||||
|
||||
override val property: KProperty1<T, *> |
||||
get() = parent |
||||
override val leaf: KProperty1<*, P> |
||||
get() = child |
||||
|
||||
} |
||||
|
||||
|
||||
/** |
||||
* Recursively construct field name for a nested property. |
||||
* |
||||
* @author Tjeu Kayim |
||||
* @author Mikhail Polivakha |
||||
* @since 4.1 |
||||
*/ |
||||
internal fun asString(property: KProperty<*>): String { |
||||
|
||||
return when (property) { |
||||
is KPropertyPath<*, *> -> |
||||
"${asString(property.property)}.${property.leaf.name}" |
||||
|
||||
else -> property.name |
||||
} |
||||
|
||||
} |
||||
|
||||
@ -0,0 +1,105 @@
@@ -0,0 +1,105 @@
|
||||
/* |
||||
* Copyright 2025-present 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. |
||||
*/ |
||||
@file:Suppress( |
||||
"UPPER_BOUND_VIOLATED_BASED_ON_JAVA_ANNOTATIONS", |
||||
"NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "UNCHECKED_CAST" |
||||
) |
||||
|
||||
package org.springframework.data.core |
||||
|
||||
import kotlin.reflect.KProperty |
||||
import kotlin.reflect.KProperty1 |
||||
import kotlin.reflect.jvm.javaField |
||||
import kotlin.reflect.jvm.javaGetter |
||||
|
||||
/** |
||||
* Extension function to compose a [TypedPropertyPath] with a [KProperty1]. |
||||
* |
||||
* @since 4.1 |
||||
*/ |
||||
fun <T : Any, P : Any?, N : Any> PropertyReference<T, P>.then(next: KProperty1<T, P?>): TypedPropertyPath<T, N> { |
||||
val nextPath = KPropertyReference.of<T, P>(next) as PropertyReference<P, N> |
||||
return TypedPropertyPaths.compose(this, nextPath) |
||||
} |
||||
|
||||
/** |
||||
* Extension function to compose a [TypedPropertyPath] with a [KProperty]. |
||||
* |
||||
* @since 4.1 |
||||
*/ |
||||
fun <T : Any, P : Any?, N : Any> PropertyReference<T, P>.then(next: KProperty<P?>): TypedPropertyPath<T, N> { |
||||
val nextPath = KPropertyReference.of<T, P>(next) as PropertyReference<P, N> |
||||
return TypedPropertyPaths.compose(this, nextPath) |
||||
} |
||||
|
||||
/** |
||||
* Helper to create [PropertyReference] from [KProperty]. |
||||
* |
||||
* @since 4.1 |
||||
*/ |
||||
class KPropertyReference { |
||||
|
||||
/** |
||||
* Companion object for static factory methods. |
||||
*/ |
||||
companion object { |
||||
|
||||
/** |
||||
* Create a [PropertyReference] from a [KProperty1] reference. |
||||
* @param property the property reference, must not be a property path. |
||||
*/ |
||||
fun <T : Any, P : Any> of(property: KProperty1<T, P?>): PropertyReference<T, P> { |
||||
return of((property as KProperty<P?>)) |
||||
} |
||||
|
||||
/** |
||||
* Create a [PropertyReference] from a collection-like [KProperty1] reference. |
||||
* @param property the property reference, must not be a property path. |
||||
*/ |
||||
@JvmName("ofMany") |
||||
fun <T : Any, P : Any> of(property: KProperty1<T, Iterable<P?>?>): PropertyReference<T, P> { |
||||
return of((property as KProperty<P?>)) |
||||
} |
||||
|
||||
/** |
||||
* Create a [PropertyReference] from a [KProperty]. |
||||
*/ |
||||
fun <T, P> of(property: KProperty<P?>): PropertyReference<T, P> { |
||||
|
||||
if (property is KPropertyPath<*, *>) { |
||||
throw IllegalArgumentException("Property reference '${property.toDotPath()}' must be a single property reference, not a property path") |
||||
} |
||||
|
||||
if (property is KProperty1<*, *>) { |
||||
|
||||
val property1 = property as KProperty1<*, *> |
||||
val owner = property1.javaField?.declaringClass |
||||
?: property1.javaGetter?.declaringClass |
||||
val metadata = PropertyReferences.KPropertyMetadata.of( |
||||
MemberDescriptor.KPropertyReferenceDescriptor.create( |
||||
owner, |
||||
property1 |
||||
) |
||||
) |
||||
return PropertyReferences.ResolvedKPropertyReference(metadata) |
||||
} |
||||
|
||||
throw IllegalArgumentException("Property '${property.name}' is not a KProperty") |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
/* |
||||
* Copyright 2025-present 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. |
||||
*/ |
||||
@file:Suppress( |
||||
"UPPER_BOUND_VIOLATED_BASED_ON_JAVA_ANNOTATIONS", |
||||
"NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "UNCHECKED_CAST" |
||||
) |
||||
|
||||
package org.springframework.data.core |
||||
|
||||
import kotlin.reflect.KProperty |
||||
import kotlin.reflect.KProperty1 |
||||
import kotlin.reflect.jvm.javaField |
||||
import kotlin.reflect.jvm.javaGetter |
||||
|
||||
/** |
||||
* Helper to create [TypedPropertyPath] from [KProperty]. |
||||
* |
||||
* @since 4.1 |
||||
*/ |
||||
internal class KTypedPropertyPath { |
||||
|
||||
/** |
||||
* Companion object for static factory methods. |
||||
*/ |
||||
companion object { |
||||
|
||||
/** |
||||
* Create a [TypedPropertyPath] from a [KProperty1]. |
||||
*/ |
||||
fun <T : Any, P : Any> of(property: KProperty1<T, P?>): TypedPropertyPath<T, P> { |
||||
return of((property as KProperty<P?>)) |
||||
} |
||||
|
||||
/** |
||||
* Create a [TypedPropertyPath] from a collection-like [KProperty1]. |
||||
*/ |
||||
@JvmName("ofMany") |
||||
fun <T : Any, P : Any> of(property: KProperty1<T, Iterable<P?>?>): TypedPropertyPath<T, P> { |
||||
return of((property as KProperty<P?>)) |
||||
} |
||||
|
||||
/** |
||||
* Create a [TypedPropertyPath] from a [KProperty]. |
||||
*/ |
||||
fun <T, P> of(property: KProperty<P?>): TypedPropertyPath<T, P> { |
||||
|
||||
if (property is KPropertyPath<*, *>) { |
||||
|
||||
val paths = property as KPropertyPath<*, *> |
||||
|
||||
val parent = of<Any, Any>(paths.property) |
||||
val child = of<Any, Any>(paths.leaf) |
||||
|
||||
return TypedPropertyPaths.compose( |
||||
parent, |
||||
child |
||||
) as TypedPropertyPath<T, P> |
||||
} |
||||
|
||||
if (property is KProperty1<*, *>) { |
||||
|
||||
val property1 = property as KProperty1<*, *> |
||||
val owner = property1.javaField?.declaringClass |
||||
?: property1.javaGetter?.declaringClass |
||||
val metadata = TypedPropertyPaths.KPropertyPathMetadata.of( |
||||
MemberDescriptor.KPropertyReferenceDescriptor.create( |
||||
owner, |
||||
property1 |
||||
) |
||||
) |
||||
return TypedPropertyPaths.ResolvedKPropertyPath(metadata) |
||||
} |
||||
|
||||
throw IllegalArgumentException("Property '${property.name}' is not a KProperty") |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import java.util.Iterator; |
||||
|
||||
/** |
||||
* TCK for {@link PropertyPath} implementations. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class PropertyPathTck { |
||||
|
||||
/** |
||||
* Verify that the given {@link PropertyPath} API behavior matches the expected one. |
||||
* |
||||
* @param actual |
||||
* @param expected |
||||
*/ |
||||
static void verify(PropertyPath actual, PropertyPath expected) { |
||||
|
||||
assertThat(actual).hasToString(expected.toString()).hasSameHashCodeAs(expected).isEqualTo(expected); |
||||
|
||||
assertThat(actual.getSegment()).isEqualTo(expected.getSegment()); |
||||
assertThat(actual.getType()).isEqualTo(expected.getType()); |
||||
|
||||
assertThat(actual.getLeafProperty()).isEqualTo(expected.getLeafProperty()); |
||||
|
||||
assertThat(actual.hasNext()).isEqualTo(expected.hasNext()); |
||||
assertThat(actual.next()).isEqualTo(expected.next()); |
||||
|
||||
Iterator<PropertyPath> actualIterator = actual.iterator(); |
||||
Iterator<PropertyPath> expectedIterator = actual.iterator(); |
||||
|
||||
assertThat(actualIterator.hasNext()).isEqualTo(expectedIterator.hasNext()); |
||||
|
||||
assertThat(actualIterator.next()).isEqualTo(actual); |
||||
assertThat(expectedIterator.next()).isEqualTo(expected); |
||||
|
||||
while (actualIterator.hasNext() && expectedIterator.hasNext()) { |
||||
|
||||
verify(actualIterator.next(), expectedIterator.next()); |
||||
assertThat(actualIterator.hasNext()).isEqualTo(expectedIterator.hasNext()); |
||||
} |
||||
|
||||
while (actual != null && expected != null && actual.hasNext() && expected.hasNext()) { |
||||
|
||||
actual = actual.next(); |
||||
expected = expected.next(); |
||||
|
||||
verify(actual, expected); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.core.convert.converter.Converter; |
||||
|
||||
/** |
||||
* Unit test {@link PropertyPathUtil}. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class PropertyPathUtilUnitTests { |
||||
|
||||
@Test |
||||
void shouldResolvePropertyPath() { |
||||
|
||||
Converter<Person, String> c = convert(Person::getName); |
||||
|
||||
System.out.println(PropertyPathUtil.resolve(c)); |
||||
} |
||||
|
||||
static <T, P, C extends Converter<T, P> & Serializable> Serializable of(C mapping) { |
||||
return mapping; |
||||
} |
||||
|
||||
static <A, B, T extends Converter<A, B> & Serializable> T convert(T converter) { |
||||
return converter; |
||||
} |
||||
|
||||
static class Person { |
||||
|
||||
private String name; |
||||
private @Nullable Integer age; |
||||
|
||||
// Getters
|
||||
public String getName() { |
||||
return name; |
||||
} |
||||
|
||||
public @Nullable Integer getAge() { |
||||
return age; |
||||
} |
||||
|
||||
} |
||||
} |
||||
@ -0,0 +1,316 @@
@@ -0,0 +1,316 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import java.util.List; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
import org.junit.jupiter.api.Nested; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.springframework.dao.InvalidDataAccessApiUsageException; |
||||
|
||||
/** |
||||
* Unit tests for {@link PropertyReference}. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class PropertyReferenceUnitTests { |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesMHSimplePath() { |
||||
assertThat(PropertyReference.of(PersonQuery::getName).getName()).isEqualTo("name"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesMHComposedPath() { |
||||
assertThat(PropertyReference.of(PersonQuery::getAddress).then(Address::getCountry).toDotPath()) |
||||
.isEqualTo("address.country"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesCollectionPath() { |
||||
assertThat(PropertyReference.ofMany(PersonQuery::getAddresses).then(Address::getCity).toDotPath()) |
||||
.isEqualTo("addresses.city"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2MethodRef") |
||||
void resolvesInitialLambdaGetter() { |
||||
assertThat(PropertyReference.of((PersonQuery person) -> person.getName()).getName()).isEqualTo("name"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2MethodRef") |
||||
void resolvesComposedLambdaGetter() { |
||||
assertThat(PropertyReference.of(PersonQuery::getAddress).then(it -> it.getCity()).toDotPath()) |
||||
.isEqualTo("address.city"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesComposedLambdaFieldAccess() { |
||||
assertThat(PropertyReference.of(PersonQuery::getAddress).then(it -> it.city).toDotPath()).isEqualTo("address.city"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesInterfaceMethodReferenceGetter() { |
||||
assertThat(PropertyReference.of(PersonProjection::getName).getName()).isEqualTo("name"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2MethodRef") |
||||
void resolvesInterfaceLambdaGetter() { |
||||
assertThat(PropertyReference.of((PersonProjection person) -> person.getName()).getName()).isEqualTo("name"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesSuperclassMethodReferenceGetter() { |
||||
assertThat(PropertyReference.of(PersonQuery::getTenant).getName()).isEqualTo("tenant"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesSuperclassLambdaGetter() { |
||||
assertThat(PropertyReference.of((PersonQuery person) -> person.getTenant()).getName()).isEqualTo("tenant"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesPrivateMethodReference() { |
||||
assertThat(PropertyReference.of(Secret::getSecret).getName()).isEqualTo("secret"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2MethodRef") |
||||
void resolvesPrivateMethodLambda() { |
||||
assertThat(PropertyReference.of((Secret secret) -> secret.getSecret()).getName()).isEqualTo("secret"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void switchingOwningTypeFails() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyReference.of((PersonQuery person) -> { |
||||
return ((SuperClass) person).getTenant(); |
||||
})); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void constructorCallsShouldFail() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyReference.of((PersonQuery person) -> new PersonQuery(person))); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void enumShouldFail() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyReference.of(NotSupported.INSTANCE)); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void returningSomethingShouldFail() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyReference.of((PropertyReference<Object, Object>) obj -> null)); |
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyReference.of((PropertyReference<Object, Object>) obj -> 1)); |
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyReference.of((PropertyReference<Object, Object>) obj -> "")); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2Lambda") |
||||
void classImplementationShouldFail() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyReference.of(new PropertyReference<Object, Object>() { |
||||
@Override |
||||
public @Nullable Object get(Object obj) { |
||||
return null; |
||||
} |
||||
})); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void constructorMethodReferenceShouldFail() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyReference.<PersonQuery, PersonQuery> of(PersonQuery::new)); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void failsResolutionWith$StrangeStuff() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyReference.of((PersonQuery person) -> { |
||||
int a = 1 + 2; |
||||
new Integer(a).toString(); |
||||
return person.getName(); |
||||
}).getName()); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void arithmeticOpsFail() { |
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> { |
||||
PropertyReference.of((PersonQuery person) -> { |
||||
int a = 1 + 2; |
||||
return person.getName(); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void failsResolvingCallingLocalMethod() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyReference.of((PersonQuery person) -> { |
||||
failsResolutionWith$StrangeStuff(); |
||||
return person.getName(); |
||||
})); |
||||
} |
||||
|
||||
@Nested |
||||
class NestedTestClass { |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2MethodRef") |
||||
void resolvesInterfaceLambdaGetter() { |
||||
assertThat(PropertyReference.of((PersonProjection person) -> person.getName()).getName()).isEqualTo("name"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesSuperclassMethodReferenceGetter() { |
||||
assertThat(PropertyReference.of(PersonQuery::getTenant).getName()).isEqualTo("tenant"); |
||||
} |
||||
|
||||
} |
||||
|
||||
// Domain entities
|
||||
|
||||
static class SuperClass { |
||||
|
||||
private int tenant; |
||||
|
||||
public int getTenant() { |
||||
return tenant; |
||||
} |
||||
|
||||
public void setTenant(int tenant) { |
||||
this.tenant = tenant; |
||||
} |
||||
} |
||||
|
||||
static class PersonQuery extends SuperClass { |
||||
|
||||
private String name; |
||||
private @Nullable Integer age; |
||||
private PersonQuery emergencyContact; |
||||
private Address address; |
||||
private List<Address> addresses; |
||||
|
||||
public PersonQuery(PersonQuery pq) {} |
||||
|
||||
public PersonQuery() {} |
||||
|
||||
// Getters
|
||||
public String getName() { |
||||
return name; |
||||
} |
||||
|
||||
public @Nullable Integer getAge() { |
||||
return age; |
||||
} |
||||
|
||||
public PersonQuery getEmergencyContact() { |
||||
return emergencyContact; |
||||
} |
||||
|
||||
public void setEmergencyContact(PersonQuery emergencyContact) { |
||||
this.emergencyContact = emergencyContact; |
||||
} |
||||
|
||||
public Address getAddress() { |
||||
return address; |
||||
} |
||||
|
||||
public List<Address> getAddresses() { |
||||
return addresses; |
||||
} |
||||
|
||||
public void setAddresses(List<Address> addresses) { |
||||
this.addresses = addresses; |
||||
} |
||||
} |
||||
|
||||
static class Address { |
||||
|
||||
String street; |
||||
String city; |
||||
private Country country; |
||||
private String secret; |
||||
|
||||
// Getters
|
||||
public String getStreet() { |
||||
return street; |
||||
} |
||||
|
||||
public String getCity() { |
||||
return city; |
||||
} |
||||
|
||||
public Country getCountry() { |
||||
return country; |
||||
} |
||||
|
||||
private String getSecret() { |
||||
return secret; |
||||
} |
||||
|
||||
private void setSecret(String secret) { |
||||
this.secret = secret; |
||||
} |
||||
} |
||||
|
||||
record Country(String name, String code) { |
||||
|
||||
} |
||||
|
||||
static class Secret { |
||||
|
||||
private String secret; |
||||
|
||||
private String getSecret() { |
||||
return secret; |
||||
} |
||||
|
||||
} |
||||
|
||||
interface PersonProjection { |
||||
|
||||
String getName(); |
||||
} |
||||
|
||||
enum NotSupported implements PropertyReference<String, String> { |
||||
|
||||
INSTANCE; |
||||
|
||||
@Override |
||||
public @Nullable String get(String obj) { |
||||
return ""; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,352 @@
@@ -0,0 +1,352 @@
|
||||
/* |
||||
* Copyright 2025-present 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.core; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import java.util.List; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
import org.junit.jupiter.api.Nested; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.params.ParameterizedTest; |
||||
import org.junit.jupiter.params.provider.Arguments; |
||||
import org.junit.jupiter.params.provider.MethodSource; |
||||
import org.springframework.dao.InvalidDataAccessApiUsageException; |
||||
|
||||
/** |
||||
* Unit tests for {@link TypedPropertyPath}. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class TypedPropertyPathUnitTests { |
||||
|
||||
@ParameterizedTest // GH-3400
|
||||
@MethodSource("propertyPaths") |
||||
void verifyTck(TypedPropertyPath<?, ?> actual, PropertyPath expected) { |
||||
PropertyPathTck.verify(actual, expected); |
||||
} |
||||
|
||||
static Stream<Arguments.ArgumentSet> propertyPaths() { |
||||
|
||||
return Stream.of( |
||||
Arguments.argumentSet("PersonQuery.name", PropertyPath.of(PersonQuery::getName), |
||||
PropertyPath.from("name", PersonQuery.class)), |
||||
Arguments.argumentSet("PersonQuery.address.country", |
||||
PropertyPath.of(PersonQuery::getAddress).then(Address::getCountry), |
||||
PropertyPath.from("address.country", PersonQuery.class)), |
||||
Arguments.argumentSet("PersonQuery.address.country.name", |
||||
PropertyPath.of(PersonQuery::getAddress).then(Address::getCountry).then(Country::name), |
||||
PropertyPath.from("address.country.name", PersonQuery.class)), |
||||
Arguments.argumentSet( |
||||
"PersonQuery.emergencyContact.address.country.name", PropertyPath.of(PersonQuery::getEmergencyContact) |
||||
.then(PersonQuery::getAddress).then(Address::getCountry).then(Country::name), |
||||
PropertyPath.from("emergencyContact.address.country.name", PersonQuery.class))); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesMHSimplePath() { |
||||
assertThat(PropertyPath.of(PersonQuery::getName).toDotPath()).isEqualTo("name"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesMHComposedPath() { |
||||
assertThat(PropertyPath.of(PersonQuery::getAddress).then(Address::getCountry).toDotPath()) |
||||
.isEqualTo("address.country"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesCollectionPath() { |
||||
assertThat(PropertyPath.ofMany(PersonQuery::getAddresses).then(Address::getCity).toDotPath()) |
||||
.isEqualTo("addresses.city"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2MethodRef") |
||||
void resolvesInitialLambdaGetter() { |
||||
assertThat(PropertyPath.of((PersonQuery person) -> person.getName()).toDotPath()).isEqualTo("name"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2MethodRef") |
||||
void resolvesComposedLambdaGetter() { |
||||
assertThat(PropertyPath.of(PersonQuery::getAddress).then(it -> it.getCity()).toDotPath()).isEqualTo("address.city"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesComposedLambdaFieldAccess() { |
||||
assertThat(PropertyPath.of(PersonQuery::getAddress).then(it -> it.city).toDotPath()).isEqualTo("address.city"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesInterfaceMethodReferenceGetter() { |
||||
assertThat(PropertyPath.of(PersonProjection::getName).toDotPath()).isEqualTo("name"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2MethodRef") |
||||
void resolvesInterfaceLambdaGetter() { |
||||
assertThat(PropertyPath.of((PersonProjection person) -> person.getName()).toDotPath()).isEqualTo("name"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesSuperclassMethodReferenceGetter() { |
||||
assertThat(PropertyPath.of(PersonQuery::getTenant).toDotPath()).isEqualTo("tenant"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesSuperclassLambdaGetter() { |
||||
assertThat(PropertyPath.of((PersonQuery person) -> person.getTenant()).toDotPath()).isEqualTo("tenant"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesPrivateMethodReference() { |
||||
assertThat(PropertyPath.of(Secret::getSecret).toDotPath()).isEqualTo("secret"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2MethodRef") |
||||
void resolvesPrivateMethodLambda() { |
||||
assertThat(PropertyPath.of((Secret secret) -> secret.getSecret()).toDotPath()).isEqualTo("secret"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void switchingOwningTypeFails() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyPath.of((PersonQuery person) -> { |
||||
return ((SuperClass) person).getTenant(); |
||||
})); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void constructorCallsShouldFail() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyPath.of((PersonQuery person) -> new PersonQuery(person))); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void enumShouldFail() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> TypedPropertyPath.of(NotSupported.INSTANCE)); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void returningSomethingShouldFail() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> TypedPropertyPath.of((TypedPropertyPath<Object, Object>) obj -> null)); |
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> TypedPropertyPath.of((TypedPropertyPath<Object, Object>) obj -> 1)); |
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> TypedPropertyPath.of((TypedPropertyPath<Object, Object>) obj -> "")); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2Lambda") |
||||
void classImplementationShouldFail() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> TypedPropertyPath.of(new TypedPropertyPath<Object, Object>() { |
||||
@Override |
||||
public @Nullable Object get(Object obj) { |
||||
return null; |
||||
} |
||||
})); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void constructorMethodReferenceShouldFail() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyPath.<PersonQuery, PersonQuery> of(PersonQuery::new)); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesMRRecordPath() { |
||||
|
||||
TypedPropertyPath<PersonQuery, String> then = PropertyPath.of(PersonQuery::getAddress).then(Address::getCountry) |
||||
.then(Country::name); |
||||
|
||||
assertThat(then.toDotPath()).isEqualTo("address.country.name"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void failsResolutionWith$StrangeStuff() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyPath.of((PersonQuery person) -> { |
||||
int a = 1 + 2; |
||||
new Integer(a).toString(); |
||||
return person.getName(); |
||||
}).toDotPath()); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void arithmeticOpsFail() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> { |
||||
PropertyPath.of((PersonQuery person) -> { |
||||
int a = 1 + 2; |
||||
return person.getName(); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void failsResolvingCallingLocalMethod() { |
||||
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) |
||||
.isThrownBy(() -> PropertyPath.of((PersonQuery person) -> { |
||||
failsResolutionWith$StrangeStuff(); |
||||
return person.getName(); |
||||
})); |
||||
} |
||||
|
||||
@Nested |
||||
class NestedTestClass { |
||||
|
||||
@Test // GH-3400
|
||||
@SuppressWarnings("Convert2MethodRef") |
||||
void resolvesInterfaceLambdaGetter() { |
||||
assertThat(PropertyPath.of((PersonProjection person) -> person.getName()).toDotPath()).isEqualTo("name"); |
||||
} |
||||
|
||||
@Test // GH-3400
|
||||
void resolvesSuperclassMethodReferenceGetter() { |
||||
assertThat(PropertyPath.of(PersonQuery::getTenant).toDotPath()).isEqualTo("tenant"); |
||||
} |
||||
|
||||
} |
||||
|
||||
// Domain entities
|
||||
|
||||
static class SuperClass { |
||||
|
||||
private int tenant; |
||||
|
||||
public int getTenant() { |
||||
return tenant; |
||||
} |
||||
|
||||
public void setTenant(int tenant) { |
||||
this.tenant = tenant; |
||||
} |
||||
} |
||||
|
||||
static class PersonQuery extends SuperClass { |
||||
|
||||
private String name; |
||||
private @Nullable Integer age; |
||||
private PersonQuery emergencyContact; |
||||
private Address address; |
||||
private List<Address> addresses; |
||||
|
||||
public PersonQuery(PersonQuery pq) {} |
||||
|
||||
public PersonQuery() {} |
||||
|
||||
// Getters
|
||||
public String getName() { |
||||
return name; |
||||
} |
||||
|
||||
public @Nullable Integer getAge() { |
||||
return age; |
||||
} |
||||
|
||||
public PersonQuery getEmergencyContact() { |
||||
return emergencyContact; |
||||
} |
||||
|
||||
public void setEmergencyContact(PersonQuery emergencyContact) { |
||||
this.emergencyContact = emergencyContact; |
||||
} |
||||
|
||||
public Address getAddress() { |
||||
return address; |
||||
} |
||||
|
||||
public List<Address> getAddresses() { |
||||
return addresses; |
||||
} |
||||
|
||||
public void setAddresses(List<Address> addresses) { |
||||
this.addresses = addresses; |
||||
} |
||||
} |
||||
|
||||
static class Address { |
||||
|
||||
String street; |
||||
String city; |
||||
private Country country; |
||||
private String secret; |
||||
|
||||
// Getters
|
||||
public String getStreet() { |
||||
return street; |
||||
} |
||||
|
||||
public String getCity() { |
||||
return city; |
||||
} |
||||
|
||||
public Country getCountry() { |
||||
return country; |
||||
} |
||||
|
||||
private String getSecret() { |
||||
return secret; |
||||
} |
||||
|
||||
private void setSecret(String secret) { |
||||
this.secret = secret; |
||||
} |
||||
} |
||||
|
||||
record Country(String name, String code) { |
||||
|
||||
} |
||||
|
||||
static class Secret { |
||||
|
||||
private String secret; |
||||
|
||||
private String getSecret() { |
||||
return secret; |
||||
} |
||||
|
||||
} |
||||
|
||||
interface PersonProjection { |
||||
|
||||
String getName(); |
||||
} |
||||
|
||||
enum NotSupported implements TypedPropertyPath<String, String> { |
||||
|
||||
INSTANCE; |
||||
|
||||
@Override |
||||
public @Nullable String get(String obj) { |
||||
return ""; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,179 @@
@@ -0,0 +1,179 @@
|
||||
/* |
||||
* Copyright 2018-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.core |
||||
|
||||
import org.assertj.core.api.Assertions.assertThat |
||||
import org.junit.Test |
||||
import org.junit.jupiter.params.ParameterizedTest |
||||
import org.junit.jupiter.params.provider.Arguments |
||||
import org.junit.jupiter.params.provider.Arguments.ArgumentSet |
||||
import org.junit.jupiter.params.provider.MethodSource |
||||
import java.util.stream.Stream |
||||
|
||||
/** |
||||
* Unit tests for [kotlin.reflect.KProperty] extensions. |
||||
* |
||||
* @author Tjeu Kayim |
||||
* @author Yoann de Martino |
||||
* @author Mark Paluch |
||||
* @author Mikhail Polivakha |
||||
*/ |
||||
class KPropertyExtensionsTests { |
||||
|
||||
@ParameterizedTest |
||||
@MethodSource("propertyPaths") |
||||
fun verifyTck(actual: TypedPropertyPath<*, *>, expected: PropertyPath) { |
||||
PropertyPathTck.verify(actual, expected) |
||||
} |
||||
|
||||
companion object { |
||||
|
||||
@JvmStatic |
||||
fun propertyPaths(): Stream<ArgumentSet> { |
||||
|
||||
return Stream.of( |
||||
Arguments.argumentSet( |
||||
"Person.name (toPath)", |
||||
Person::name.toPropertyPath(), |
||||
PropertyPath.from("name", Person::class.java) |
||||
), |
||||
Arguments.argumentSet( |
||||
"Person.address.country.name (toPath)", |
||||
(Person::address / Address::country / Country::name).toPropertyPath(), |
||||
PropertyPath.from("address.country.name", Person::class.java) |
||||
), |
||||
Arguments.argumentSet( |
||||
"Person.addresses.country.name (toPath)", |
||||
(Person::addresses / Address::country / Country::name).toPropertyPath(), |
||||
PropertyPath.from("addresses.country.name", Person::class.java) |
||||
) |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Test // DATACMNS-1835 |
||||
fun `Convert normal KProperty to field name`() { |
||||
|
||||
val property = Book::title.toDotPath() |
||||
|
||||
assertThat(property).isEqualTo("title") |
||||
} |
||||
|
||||
@Test // DATACMNS-1835 |
||||
fun `Convert nested KProperty to field name`() { |
||||
|
||||
val property = (Book::author / Author::name).toDotPath() |
||||
|
||||
assertThat(property).isEqualTo("author.name") |
||||
} |
||||
|
||||
@Test // GH-3010 |
||||
fun `Convert from Iterable nested KProperty to field name`() { |
||||
|
||||
val property = (Author::books / Book::title).toDotPath() |
||||
|
||||
assertThat(property).isEqualTo("books.title") |
||||
} |
||||
|
||||
@Test // GH-3010 |
||||
fun `Convert from Iterable nested Iterable Property to field name`() { |
||||
|
||||
val property = (Author::books / Book::author / Author::name).toDotPath() |
||||
|
||||
assertThat(property).isEqualTo("books.author.name") |
||||
} |
||||
|
||||
@Test // DATACMNS-1835 |
||||
fun `Convert double nested KProperty to field name`() { |
||||
|
||||
class Entity(val book: Book) |
||||
|
||||
val property = (Entity::book / Book::author / Author::name).toDotPath() |
||||
|
||||
assertThat(property).isEqualTo("book.author.name") |
||||
} |
||||
|
||||
@Test // DATACMNS-1835 |
||||
fun `Convert triple nested KProperty to field name`() { |
||||
|
||||
class Entity(val book: Book) |
||||
class AnotherEntity(val entity: Entity) |
||||
|
||||
val property = |
||||
(AnotherEntity::entity / Entity::book / Book::author / Author::name).toDotPath() |
||||
|
||||
assertThat(property).isEqualTo("entity.book.author.name") |
||||
} |
||||
|
||||
@Test // DATACMNS-1835 |
||||
fun `Convert triple nested KProperty to property path using toDotPath`() { |
||||
|
||||
class Entity(val book: Book) |
||||
class AnotherEntity(val entity: Entity) |
||||
|
||||
val property = |
||||
(AnotherEntity::entity / Entity::book / Book::author / Author::name).toDotPath() |
||||
|
||||
assertThat(property).isEqualTo("entity.book.author.name") |
||||
} |
||||
|
||||
@Test // DATACMNS-1835 |
||||
fun `Convert simple KProperty to property path using toDotPath`() { |
||||
|
||||
class AnotherEntity(val entity: String) |
||||
|
||||
val property = AnotherEntity::entity.toDotPath() |
||||
|
||||
assertThat(property).isEqualTo("entity") |
||||
} |
||||
|
||||
@Test // DATACMNS-1835 |
||||
fun `Convert nested KProperty to field name using toDotPath()`() { |
||||
|
||||
val property = (Book::author / Author::name).toDotPath() |
||||
|
||||
assertThat(property).isEqualTo("author.name") |
||||
} |
||||
|
||||
@Test // DATACMNS-1835 |
||||
fun `Convert nullable KProperty to field name`() { |
||||
|
||||
class Cat(val name: String?) |
||||
class Owner(val cat: Cat?) |
||||
|
||||
val property = (Owner::cat / Cat::name).toDotPath() |
||||
assertThat(property).isEqualTo("cat.name") |
||||
} |
||||
|
||||
class Book(val title: String, val author: Author) |
||||
class Author(val name: String, val books: List<Book>) |
||||
|
||||
class Person { |
||||
var name: String? = null |
||||
var age: Int = 0 |
||||
var address: Address? = null |
||||
var addresses: List<Address> = emptyList() |
||||
} |
||||
|
||||
class Address { |
||||
var city: String? = null |
||||
var street: String? = null |
||||
var country: Country? = null |
||||
} |
||||
|
||||
data class Country(val name: String) |
||||
|
||||
} |
||||
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
package org.springframework.data.core |
||||
|
||||
import org.assertj.core.api.Assertions.assertThat |
||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException |
||||
import org.junit.jupiter.api.Test |
||||
|
||||
/** |
||||
* Unit tests for [KPropertyReference] and related functionality. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class KPropertyReferenceUnitTests { |
||||
|
||||
@Test // GH-3400 |
||||
fun shouldCreatePropertyReference() { |
||||
|
||||
val path = KPropertyReference.of(Person::name) |
||||
|
||||
assertThat(path.name).isEqualTo("name") |
||||
} |
||||
|
||||
@Test // GH-3400 |
||||
fun shouldComposePropertyPath() { |
||||
|
||||
val path = KPropertyReference.of(Person::address).then(Address::city) |
||||
|
||||
assertThat(path.toDotPath()).isEqualTo("address.city") |
||||
} |
||||
|
||||
@Test // GH-3400 |
||||
fun shouldComposeManyPropertyPath() { |
||||
|
||||
val path = KPropertyReference.of(Person::addresses).then(Address::city) |
||||
|
||||
assertThat(path.toDotPath()).isEqualTo("addresses.city") |
||||
} |
||||
|
||||
@Test // GH-3400 |
||||
fun composedReferenceCreationShouldFail() { |
||||
assertThatIllegalArgumentException().isThrownBy { |
||||
PropertyReference.property( |
||||
Person::address / Address::city |
||||
) |
||||
} |
||||
assertThatIllegalArgumentException().isThrownBy { KPropertyReference.of(Person::address / Address::city) } |
||||
} |
||||
|
||||
class Person { |
||||
var name: String? = null |
||||
var age: Int = 0 |
||||
var address: Address? = null |
||||
var addresses: List<Address> = emptyList() |
||||
var emergencyContact: Person? = null |
||||
} |
||||
|
||||
class Address { |
||||
var city: String? = null |
||||
var street: String? = null |
||||
var country: Country? = null |
||||
} |
||||
|
||||
data class Country(val name: String) |
||||
} |
||||
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
package org.springframework.data.core |
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest |
||||
import org.junit.jupiter.params.provider.Arguments |
||||
import org.junit.jupiter.params.provider.Arguments.ArgumentSet |
||||
import org.junit.jupiter.params.provider.MethodSource |
||||
import java.util.stream.Stream |
||||
|
||||
/** |
||||
* Unit tests for [KPropertyPath] and related functionality. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class KTypedPropertyPathUnitTests { |
||||
|
||||
@ParameterizedTest |
||||
@MethodSource("propertyPaths") |
||||
fun verifyTck(actual: TypedPropertyPath<*, *>, expected: PropertyPath) { |
||||
PropertyPathTck.verify(actual, expected) |
||||
} |
||||
|
||||
companion object { |
||||
|
||||
@JvmStatic |
||||
fun propertyPaths(): Stream<ArgumentSet> { |
||||
|
||||
return Stream.of( |
||||
Arguments.argumentSet( |
||||
"Person.name (toPath)", |
||||
Person::name.toPropertyPath(), |
||||
PropertyPath.from("name", Person::class.java) |
||||
), |
||||
Arguments.argumentSet( |
||||
"Person.address.country", |
||||
(Person::address / Address::country).toPropertyPath(), |
||||
PropertyPath.from("address.country", Person::class.java) |
||||
), |
||||
Arguments.argumentSet( |
||||
"Person.address.country.name", |
||||
(Person::address / Address::country / Country::name).toPropertyPath(), |
||||
PropertyPath.from("address.country.name", Person::class.java) |
||||
), |
||||
Arguments.argumentSet( |
||||
"Person.address.country.name (toPath)", |
||||
(Person::address / Address::country / Country::name).toPropertyPath(), |
||||
PropertyPath.from("address.country.name", Person::class.java) |
||||
), |
||||
Arguments.argumentSet( |
||||
"Person.emergencyContact.address.country.name", |
||||
(Person::emergencyContact / Person::address / Address::country / Country::name).toPropertyPath(), |
||||
PropertyPath.from( |
||||
"emergencyContact.address.country.name", |
||||
Person::class.java |
||||
) |
||||
) |
||||
) |
||||
} |
||||
} |
||||
|
||||
class Person { |
||||
var name: String? = null |
||||
var age: Int = 0 |
||||
var address: Address? = null |
||||
var addresses: List<Address> = emptyList() |
||||
var emergencyContact: Person? = null |
||||
} |
||||
|
||||
class Address { |
||||
var city: String? = null |
||||
var street: String? = null |
||||
var country: Country? = null |
||||
} |
||||
|
||||
data class Country(val name: String) |
||||
} |
||||
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
package org.springframework.data.core |
||||
|
||||
import org.assertj.core.api.Assertions.assertThat |
||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException |
||||
import org.junit.jupiter.api.Test |
||||
|
||||
/** |
||||
* Kotlin unit tests for [PropertyReference] and related functionality. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class PropertyReferenceKtUnitTests { |
||||
|
||||
@Test // GH-3400 |
||||
fun shouldSupportPropertyReference() { |
||||
assertThat(PropertyReference.property(Person::address).name).isEqualTo("address") |
||||
} |
||||
|
||||
@Test // GH-3400 |
||||
fun resolutionShouldFailForComposedPropertyPath() { |
||||
assertThatIllegalArgumentException() |
||||
.isThrownBy { PropertyReference.property(Person::address / Address::city) } |
||||
} |
||||
|
||||
class Person { |
||||
var name: String? = null |
||||
var age: Int = 0 |
||||
var address: Address? = null |
||||
} |
||||
|
||||
class Address { |
||||
var city: String? = null |
||||
var street: String? = null |
||||
} |
||||
|
||||
data class Country(val name: String) |
||||
|
||||
} |
||||
@ -0,0 +1,117 @@
@@ -0,0 +1,117 @@
|
||||
package org.springframework.data.core |
||||
|
||||
import org.assertj.core.api.Assertions.assertThat |
||||
import org.junit.jupiter.api.Test |
||||
import org.junit.jupiter.params.ParameterizedTest |
||||
import org.junit.jupiter.params.provider.Arguments |
||||
import org.junit.jupiter.params.provider.Arguments.ArgumentSet |
||||
import org.junit.jupiter.params.provider.MethodSource |
||||
import java.util.stream.Stream |
||||
|
||||
/** |
||||
* Kotlin unit tests for [TypedPropertyPath] and related functionality. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class TypedPropertyPathKtUnitTests { |
||||
|
||||
@ParameterizedTest |
||||
@MethodSource("propertyPaths") |
||||
fun verifyTck(actual: TypedPropertyPath<*, *>?, expected: PropertyPath) { |
||||
PropertyPathTck.verify(actual, expected) |
||||
} |
||||
|
||||
companion object { |
||||
|
||||
@JvmStatic |
||||
fun propertyPaths(): Stream<ArgumentSet> { |
||||
|
||||
return Stream.of( |
||||
Arguments.argumentSet( |
||||
"Person.name", |
||||
TypedPropertyPath.path(Person::name), |
||||
PropertyPath.from("name", Person::class.java) |
||||
), |
||||
Arguments.argumentSet( |
||||
"Person.address.country", |
||||
TypedPropertyPath.path<Person, Address>(Person::address) |
||||
.then(Address::country), |
||||
PropertyPath.from("address.country", Person::class.java) |
||||
), |
||||
Arguments.argumentSet( |
||||
"Person.address.country.name", |
||||
TypedPropertyPath.path<Person, Address>(Person::address) |
||||
.then<Country>(Address::country).then(Country::name), |
||||
PropertyPath.from("address.country.name", Person::class.java) |
||||
), |
||||
Arguments.argumentSet( |
||||
"Person.emergencyContact.address.country.name", |
||||
TypedPropertyPath.path<Person, Person>(Person::emergencyContact) |
||||
.then<Address>(Person::address).then<Country>(Address::country) |
||||
.then(Country::name), |
||||
PropertyPath.from( |
||||
"emergencyContact.address.country.name", |
||||
Person::class.java |
||||
) |
||||
) |
||||
) |
||||
} |
||||
} |
||||
|
||||
@Test // GH-3400 |
||||
fun shouldSupportPropertyReference() { |
||||
|
||||
assertThat( |
||||
TypedPropertyPath.path(Person::address).toDotPath() |
||||
).isEqualTo("address") |
||||
} |
||||
|
||||
@Test // GH-3400 |
||||
fun shouldSupportComposedPropertyReference() { |
||||
|
||||
val path = TypedPropertyPath.path<Person, Address>(Person::address) |
||||
.then(Address::city); |
||||
assertThat(path.toDotPath()).isEqualTo("address.city") |
||||
} |
||||
|
||||
@Test // GH-3400 |
||||
fun shouldSupportPropertyLambda() { |
||||
assertThat(TypedPropertyPath.path<Person, Address> { it.address } |
||||
.toDotPath()).isEqualTo("address") |
||||
assertThat(TypedPropertyPath.path<Person, Address> { foo -> foo.address } |
||||
.toDotPath()).isEqualTo("address") |
||||
} |
||||
|
||||
@Test // GH-3400 |
||||
fun shouldSupportComposedPropertyLambda() { |
||||
|
||||
val path = TypedPropertyPath.path<Person, Address> { it.address }; |
||||
assertThat(path.then { it.city }.toDotPath()).isEqualTo("address.city") |
||||
} |
||||
|
||||
@Test // GH-3400 |
||||
fun shouldSupportComposedKProperty() { |
||||
|
||||
val path = TypedPropertyPath.path(Person::address / Address::city); |
||||
assertThat(path.toDotPath()).isEqualTo("address.city") |
||||
|
||||
val otherPath = TypedPropertyPath.of(Person::address / Address::city); |
||||
assertThat(otherPath.toDotPath()).isEqualTo("address.city") |
||||
} |
||||
|
||||
class Person { |
||||
var name: String? = null |
||||
var age: Int = 0 |
||||
var address: Address? = null |
||||
var emergencyContact: Person? = null |
||||
} |
||||
|
||||
class Address { |
||||
var city: String? = null |
||||
var street: String? = null |
||||
var country: Country? = null |
||||
} |
||||
|
||||
data class Country(val name: String) |
||||
|
||||
} |
||||
Loading…
Reference in new issue