Skip to content

Commit 209ebfd

Browse files
committed
Introduce TypedPropertyPath.
1 parent 4de4c51 commit 209ebfd

File tree

6 files changed

+1093
-11
lines changed

6 files changed

+1093
-11
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@
3434
*/
3535
public interface PropertyPath extends Streamable<PropertyPath> {
3636

37+
/**
38+
* Syntax sugar to create a {@link TypedPropertyPath} from an existing one, ideal for method handles.
39+
*
40+
* @param propertyPath
41+
* @return
42+
* @param <T> owning type.
43+
* @param <R> property type.
44+
* @since xxx
45+
*/
46+
public static <T, R> TypedPropertyPath<T, R> of(TypedPropertyPath<T, R> propertyPath) {
47+
return TypedPropertyPath.of(propertyPath);
48+
}
49+
3750
/**
3851
* Returns the owning type of the {@link PropertyPath}.
3952
*

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,16 +148,6 @@ public boolean isCollection() {
148148
return isCollection;
149149
}
150150

151-
@Override
152-
public SimplePropertyPath nested(String path) {
153-
154-
Assert.hasText(path, "Path must not be null or empty");
155-
156-
String lookup = toDotPath().concat(".").concat(path);
157-
158-
return SimplePropertyPath.from(lookup, owningType);
159-
}
160-
161151
@Override
162152
public Iterator<PropertyPath> iterator() {
163153

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mapping;
17+
18+
import java.io.Serializable;
19+
import java.util.Collections;
20+
import java.util.Iterator;
21+
22+
import org.jspecify.annotations.Nullable;
23+
import org.springframework.data.util.TypeInformation;
24+
25+
/**
26+
* Type-safe representation of a property path expressed through method references.
27+
* <p>
28+
* This functional interface extends {@link PropertyPath} to provide compile-time type safety when declaring property
29+
* paths. Instead of using {@link PropertyPath#from(String, TypeInformation) string-based property paths} that represent
30+
* references to properties textually and that are prone to refactoring issues, {@code TypedPropertyPath} leverages
31+
* Java's declarative method references and lambda expressions to ensure type-safe property access.
32+
* <p>
33+
* Typed property paths can be created directly they are accepted used or conveniently using the static factory method
34+
* {@link #of(TypedPropertyPath)} with method references:
35+
*
36+
* <pre class="code">
37+
* PropertyPath.of(Person::getName);
38+
* </pre>
39+
*
40+
* Property paths can be composed to navigate nested properties using {@link #then(TypedPropertyPath)}:
41+
*
42+
* <pre class="code">
43+
* PropertyPath.of(Person::getAddress).then(Address::getCountry).then(Country::getName);
44+
* </pre>
45+
* <p>
46+
* The interface maintains type information throughout the property path chain: the {@code T} type parameter represents
47+
* its owning type (root type for composed paths), while {@code P} represents the property value type at this path
48+
* segment.
49+
* <p>
50+
* As a functional interface, {@code TypedPropertyPath} should be implemented as method reference (recommended).
51+
* Alternatively, the interface can be implemented as lambda extracting a property value from an object of type
52+
* {@code T}. Implementations must ensure that the method reference or lambda correctly represents a property access
53+
* through a method invocation or by field access. Arbitrary calls to non-getter methods (i.e. methods accepting
54+
* parameters or arbitrary method calls on types other than the owning type are not allowed and will fail with
55+
* {@link org.springframework.dao.InvalidDataAccessApiUsageException}.
56+
* <p>
57+
* Note that using lambda expressions requires bytecode analysis of the declaration site classes and therefore presence
58+
* of their class files.
59+
*
60+
* @param <T> the owning type of the property path segment, but typically the root type for composed property paths.
61+
* @param <P> the property value type at this path segment.
62+
* @author Mark Paluch
63+
* @see PropertyPath
64+
* @see #of(TypedPropertyPath)
65+
* @see #then(TypedPropertyPath)
66+
*/
67+
@FunctionalInterface
68+
public interface TypedPropertyPath<T, P> extends PropertyPath, Serializable {
69+
70+
/**
71+
* Syntax sugar to create a {@link TypedPropertyPath} from a method reference or lambda.
72+
* <p>
73+
* This method returns a resolved {@link TypedPropertyPath} by introspecting the given lambda.
74+
*
75+
* @param lambda the method reference or lambda.
76+
* @param <T> owning type.
77+
* @param <P> property type.
78+
* @return the typed property path.
79+
*/
80+
static <T, P> TypedPropertyPath<T, P> of(TypedPropertyPath<T, P> lambda) {
81+
return TypedPropertyPaths.of(lambda);
82+
}
83+
84+
/**
85+
* Get the property value for the given object.
86+
*
87+
* @param obj the object to get the property value from.
88+
* @return the property value.
89+
*/
90+
@Nullable
91+
P get(T obj);
92+
93+
@Override
94+
default TypeInformation<?> getOwningType() {
95+
return TypedPropertyPaths.getPropertyPathInformation(this).owner();
96+
}
97+
98+
@Override
99+
default String getSegment() {
100+
return TypedPropertyPaths.getPropertyPathInformation(this).property().getName();
101+
}
102+
103+
@Override
104+
default TypeInformation<?> getTypeInformation() {
105+
return TypedPropertyPaths.getPropertyPathInformation(this).propertyType();
106+
}
107+
108+
@Override
109+
@Nullable
110+
default PropertyPath next() {
111+
return null;
112+
}
113+
114+
@Override
115+
default boolean hasNext() {
116+
return false;
117+
}
118+
119+
@Override
120+
default Iterator<PropertyPath> iterator() {
121+
return Collections.singletonList((PropertyPath) this).iterator();
122+
}
123+
124+
/**
125+
* Extend the property path by appending the {@code next} path segment and returning a new property path instance..
126+
*
127+
* @param next the next property path segment accepting a property path owned by the {@code P} type.
128+
* @param <N> the new property value type.
129+
* @return a new composed {@code TypedPropertyPath}.
130+
*/
131+
default <N> TypedPropertyPath<T, N> then(TypedPropertyPath<P, N> next) {
132+
return TypedPropertyPaths.compose(this, of(next));
133+
}
134+
}

0 commit comments

Comments
 (0)