Skip to content

Commit 3a186c1

Browse files
committed
Collection support.
1 parent 3cd52da commit 3a186c1

File tree

5 files changed

+97
-18
lines changed

5 files changed

+97
-18
lines changed

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.util.regex.Pattern;
2020

2121
import org.jspecify.annotations.Nullable;
22-
2322
import org.springframework.data.util.Streamable;
2423
import org.springframework.util.Assert;
2524

@@ -43,11 +42,27 @@ public interface PropertyPath extends Streamable<PropertyPath> {
4342
* @param <T> owning type.
4443
* @param <P> property type.
4544
* @return the typed property path.
45+
* @since 4.1
4646
*/
4747
static <T, P> TypedPropertyPath<T, P> of(TypedPropertyPath<T, P> propertyPath) {
4848
return TypedPropertyPaths.of(propertyPath);
4949
}
5050

51+
/**
52+
* Syntax sugar to create a {@link TypedPropertyPath} from a method reference or lambda for a collection property.
53+
* <p>
54+
* This method returns a resolved {@link TypedPropertyPath} by introspecting the given method reference or lambda.
55+
*
56+
* @param propertyPath the method reference or lambda.
57+
* @param <T> owning type.
58+
* @param <P> property type.
59+
* @return the typed property path.
60+
* @since 4.1
61+
*/
62+
static <T, P> TypedPropertyPath<T, P> ofMany(TypedPropertyPath<T, ? extends Iterable<P>> propertyPath) {
63+
return (TypedPropertyPath) TypedPropertyPaths.of(propertyPath);
64+
}
65+
5166
/**
5267
* Returns the owning type of the {@link PropertyPath}.
5368
*

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

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,41 @@
2222
import org.jspecify.annotations.Nullable;
2323

2424
/**
25-
* Type-safe representation of a property path using getter method references or lambda expressions.
25+
* Interface providing type-safe property path navigation through method references or lambda expressions.
2626
* <p>
2727
* This functional interface extends {@link PropertyPath} to provide compile-time type safety and refactoring support.
2828
* Instead of using {@link PropertyPath#from(String, TypeInformation) string-based property paths} for textual property
2929
* representation that are easy to miss when changing the domain model, {@code TypedPropertyPath} leverages Java's
3030
* declarative method references and lambda expressions to ensure type-safe property access.
3131
* <p>
32-
* Typed property paths can be created directly they are accepted used or conveniently using the static factory method
33-
* {@link #of(TypedPropertyPath)} with method references:
32+
* Create a typed property path using the static factory method {@link #of(TypedPropertyPath)} with a method reference
33+
* or lambda, for example:
3434
*
3535
* <pre class="code">
36-
* PropertyPath.of(Person::getName);
36+
* TypedPropertyPath&lt;Person, String&gt; name = TypedPropertyPath.of(Person::getName);
3737
* </pre>
38-
*
39-
* Property paths can be composed to navigate nested properties using {@link #then(TypedPropertyPath)}:
38+
*
39+
* The resulting object can be used to obtain the {@link #toDotPath() dot-path} and to interact with the targetting
40+
* property. Typed paths allow for composition to navigate nested object structures using
41+
* {@link #then(TypedPropertyPath)}:
4042
*
4143
* <pre class="code">
42-
* PropertyPath.of(Person::getAddress).then(Address::getCity);
44+
* TypedPropertyPath&lt;Person, String&gt; city = TypedPropertyPath.of(Person::getAddress).then(Address::getCity);
4345
* </pre>
4446
* <p>
45-
* The interface maintains type information throughout the property path chain: the {@code T} type parameter represents
46-
* its owning type (root type for composed paths), while {@code P} represents the property value type at this path
47-
* segment.
47+
* The generic type parameters preserve type information across the property path chain: {@code T} represents the owning
48+
* type of the current segment (or the root type for composed paths), while {@code P} represents the property value type
49+
* at this segment. Composition automatically flows type information forward, ensuring that {@code then()} preserves the
50+
* full chain's type safety.
4851
* <p>
49-
* Use method references (recommended) or lambdas that access a property getter to implement {@code TypedPropertyPath}.
50-
* Usage of constructor references, method calls with parameters, and complex expressions results in
51-
* {@link org.springframework.dao.InvalidDataAccessApiUsageException}. In contrast to method references, introspection
52-
* of lambda expressions requires bytecode analysis of the declaration site classes and therefore presence of their
53-
* class files.
52+
* Implement {@code TypedPropertyPath} using method references (strongly recommended) or lambdas that directly access a
53+
* property getter. Constructor references, method calls with parameters, and complex expressions are not supported and
54+
* result in {@link org.springframework.dao.InvalidDataAccessApiUsageException}. Unlike method references, introspection
55+
* of lambda expressions requires bytecode analysis of the declaration site classes and thus depends on their
56+
* availability at runtime.
5457
*
55-
* @param <T> the owning type of the property path segment, root type for composed paths.
56-
* @param <P> the property type at this path segment.
58+
* @param <T> the owning type of this path segment; the root type for composed paths.
59+
* @param <P> the property value type at this path segment.
5760
* @author Mark Paluch
5861
* @since 4.1
5962
* @see PropertyPath#of(TypedPropertyPath)
@@ -76,6 +79,21 @@ public interface TypedPropertyPath<T, P> extends PropertyPath, Serializable {
7679
return TypedPropertyPaths.of(propertyPath);
7780
}
7881

82+
/**
83+
* Syntax sugar to create a {@link TypedPropertyPath} from a method reference or lambda for a collection property.
84+
* <p>
85+
* This method returns a resolved {@link TypedPropertyPath} by introspecting the given method reference or lambda.
86+
*
87+
* @param propertyPath the method reference or lambda.
88+
* @param <T> owning type.
89+
* @param <P> property type.
90+
* @return the typed property path.
91+
* @since 4.1
92+
*/
93+
static <T, P> TypedPropertyPath<T, P> ofMany(TypedPropertyPath<T, ? extends Iterable<P>> propertyPath) {
94+
return (TypedPropertyPath) TypedPropertyPaths.of(propertyPath);
95+
}
96+
7997
/**
8098
* Get the property value for the given object.
8199
*
@@ -128,4 +146,17 @@ default Iterator<PropertyPath> iterator() {
128146
return TypedPropertyPaths.compose(this, of(next));
129147
}
130148

149+
/**
150+
* Extend the property path by appending the {@code next} path segment and returning a new property path instance.
151+
*
152+
* @param next the next property path segment as method reference or lambda accepting the owner object {@code P} type
153+
* and returning {@code N} as result of accessing a property.
154+
* @param <N> the new property value type.
155+
* @return a new composed {@code TypedPropertyPath}.
156+
*/
157+
default <N extends @Nullable Object> TypedPropertyPath<T, N> thenMany(
158+
TypedPropertyPath<P, ? extends Iterable<N>> next) {
159+
return (TypedPropertyPath) TypedPropertyPaths.compose(this, of(next));
160+
}
161+
131162
}

src/main/kotlin/org/springframework/data/core/TypedPropertyPathExtensions.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ class KTypedPropertyPath {
6666
return of((property as KProperty<P?>))
6767
}
6868

69+
/**
70+
* Create a [TypedPropertyPath] from a collection-like [KProperty1].
71+
*/
72+
@JvmName("ofMany")
73+
fun <T : Any, P : Any> of(property: KProperty1<T, Iterable<P>>): TypedPropertyPath<T, P> {
74+
return of((property as KProperty<P?>))
75+
}
76+
6977
/**
7078
* Create a [TypedPropertyPath] from a [KProperty].
7179
*/

src/test/java/org/springframework/data/core/TypedPropertyPathUnitTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import java.util.List;
21+
2022
import org.jspecify.annotations.Nullable;
2123
import org.junit.jupiter.api.Nested;
2224
import org.junit.jupiter.api.Test;
@@ -63,6 +65,12 @@ void resolvesMHComposedPath() {
6365
.isEqualTo("address.country");
6466
}
6567

68+
@Test
69+
void resolvesCollectionPath() {
70+
assertThat(PropertyPath.ofMany(PersonQuery::getAddresses).then(Address::getCity).toDotPath())
71+
.isEqualTo("addresses.city");
72+
}
73+
6674
@Test
6775
void resolvesInitialLambdaGetter() {
6876
assertThat(PropertyPath.of((PersonQuery person) -> person.getName()).toDotPath()).isEqualTo("name");
@@ -235,6 +243,7 @@ static public class PersonQuery extends SuperClass {
235243
private String name;
236244
private Integer age;
237245
private Address address;
246+
private List<Address> addresses;
238247

239248
public PersonQuery(PersonQuery pq) {}
240249

@@ -252,6 +261,14 @@ public Integer getAge() {
252261
public Address getAddress() {
253262
return address;
254263
}
264+
265+
public List<Address> getAddresses() {
266+
return addresses;
267+
}
268+
269+
public void setAddresses(List<Address> addresses) {
270+
this.addresses = addresses;
271+
}
255272
}
256273

257274
class Address {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ class KTypedPropertyPathUnitTests {
2929
assertThat(path.toDotPath()).isEqualTo("author.name")
3030
}
3131

32+
@Test
33+
fun shouldComposeManyPropertyPath() {
34+
35+
val path = KTypedPropertyPath.of(Author::books).then(Book::title)
36+
37+
assertThat(path.toDotPath()).isEqualTo("books.title")
38+
}
39+
3240
@Test
3341
fun shouldCreateComposed() {
3442

0 commit comments

Comments
 (0)