Skip to content

Commit 12399a4

Browse files
committed
Fix property path composition, introduce TCK for PropertyPath, simplify equality and hashing checks.
1 parent c39b653 commit 12399a4

File tree

11 files changed

+442
-151
lines changed

11 files changed

+442
-151
lines changed

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

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ static <T, P> TypedPropertyPath<T, P> of(TypedPropertyPath<T, P> propertyPath) {
5959
* @return the typed property path.
6060
* @since 4.1
6161
*/
62+
@SuppressWarnings({ "unchecked", "rawtypes" })
6263
static <T, P> TypedPropertyPath<T, P> ofMany(TypedPropertyPath<T, ? extends Iterable<P>> propertyPath) {
6364
return (TypedPropertyPath) TypedPropertyPaths.of(propertyPath);
6465
}
@@ -71,22 +72,25 @@ static <T, P> TypedPropertyPath<T, P> ofMany(TypedPropertyPath<T, ? extends Iter
7172
TypeInformation<?> getOwningType();
7273

7374
/**
74-
* Returns the first part of the {@link PropertyPath}. For example:
75+
* Returns the current property path segment (i.e. first part of {@link #toDotPath()}).
76+
* <p>
77+
* For example:
7578
*
7679
* <pre class="code">
7780
* PropertyPath.from("a.b.c", Some.class).getSegment();
7881
* </pre>
7982
*
8083
* results in {@code a}.
8184
*
82-
* @return the name will never be {@literal null}.
85+
* @return the current property path segment.
8386
*/
8487
String getSegment();
8588

8689
/**
87-
* Returns the leaf property of the {@link PropertyPath}.
90+
* Returns the leaf property of the {@link PropertyPath}. Either this property if the path ends here or the last
91+
* property in the chain.
8892
*
89-
* @return will never be {@literal null}.
93+
* @return leaf property.
9094
*/
9195
default PropertyPath getLeafProperty() {
9296

@@ -103,57 +107,72 @@ default PropertyPath getLeafProperty() {
103107
* Returns the type of the leaf property of the current {@link PropertyPath}.
104108
*
105109
* @return will never be {@literal null}.
110+
* @see #getLeafProperty()
106111
*/
107112
default Class<?> getLeafType() {
108113
return getLeafProperty().getType();
109114
}
110115

111116
/**
112-
* Returns the actual type of the property. Will return the plain resolved type for simple properties, the component
113-
* type for any {@link Iterable} or the value type of a {@link java.util.Map}.
117+
* Returns the actual type of the property at this segment. Will return the plain resolved type for simple properties,
118+
* the component type for any {@link Iterable} or the value type of {@link java.util.Map} properties.
114119
*
115120
* @return the actual type of the property.
121+
* @see #getTypeInformation()
122+
* @see TypeInformation#getRequiredActualType()
116123
*/
117124
default Class<?> getType() {
118125
return getTypeInformation().getRequiredActualType().getType();
119126
}
120127

121128
/**
122-
* Returns the type information of the property.
129+
* Returns the type information for the property at this segment.
123130
*
124-
* @return the actual type of the property.
131+
* @return the type information for the property at this segment.
125132
*/
126133
TypeInformation<?> getTypeInformation();
127134

128135
/**
129-
* Returns the {@link PropertyPath} path that results from removing the first element of the current one. For example:
136+
* Returns whether the current property path segment is a collection.
137+
*
138+
* @return {@literal true} if the current property path segment is a collection.
139+
* @see #getTypeInformation()
140+
* @see TypeInformation#isCollectionLike()
141+
*/
142+
default boolean isCollection() {
143+
return getTypeInformation().isCollectionLike();
144+
}
145+
146+
/**
147+
* Returns the next {@code PropertyPath} segment in the property path chain.
130148
*
131149
* <pre class="code">
132150
* PropertyPath.from("a.b.c", Some.class).next().toDotPath();
133151
* </pre>
134152
*
135153
* results in the output: {@code b.c}
136154
*
137-
* @return the next nested {@link PropertyPath} or {@literal null} if no nested {@link PropertyPath} available.
155+
* @return the next {@code PropertyPath} or {@literal null} if the path does not contain further segments.
138156
* @see #hasNext()
139157
*/
140158
@Nullable
141159
PropertyPath next();
142160

143161
/**
144-
* Returns whether there is a nested {@link PropertyPath}. If this returns {@literal true} you can expect
145-
* {@link #next()} to return a non- {@literal null} value.
162+
* Returns {@literal true} if the property path contains further segments or {@literal false} if the path ends at this
163+
* segment.
146164
*
147-
* @return
165+
* @return {@literal true} if the property path contains further segments or {@literal false} if the path ends at this
166+
* segment.
148167
*/
149168
default boolean hasNext() {
150169
return next() != null;
151170
}
152171

153172
/**
154-
* Returns the {@link PropertyPath} in dot notation.
173+
* Returns the {@code PropertyPath} in dot notation.
155174
*
156-
* @return the {@link PropertyPath} in dot notation.
175+
* @return the {@code PropertyPath} in dot notation.
157176
*/
158177
default String toDotPath() {
159178

@@ -162,16 +181,7 @@ default String toDotPath() {
162181
}
163182

164183
/**
165-
* Returns whether the {@link PropertyPath} is actually a collection.
166-
*
167-
* @return {@literal true} whether the {@link PropertyPath} is actually a collection.
168-
*/
169-
default boolean isCollection() {
170-
return getTypeInformation().isCollectionLike();
171-
}
172-
173-
/**
174-
* Returns the {@link PropertyPath} for the path nested under the current property.
184+
* Returns the {@code PropertyPath} for the path nested under the current property.
175185
*
176186
* @param path must not be {@literal null} or empty.
177187
* @return will never be {@literal null}.
@@ -186,8 +196,7 @@ default PropertyPath nested(String path) {
186196
}
187197

188198
/**
189-
* Returns an {@link Iterator Iterator of PropertyPath} that iterates over all the partial property paths with the
190-
* same leaf type but decreasing length. For example:
199+
* Returns an {@link Iterator Iterator of PropertyPath} that iterates over all property path segments. For example:
191200
*
192201
* <pre class="code">
193202
* PropertyPath propertyPath = PropertyPath.from("a.b.c", Some.class);
@@ -197,9 +206,9 @@ default PropertyPath nested(String path) {
197206
* results in the dot paths:
198207
*
199208
* <pre class="code">
200-
* a.b.c
201-
* b.c
202-
* c
209+
* a.b.c (this object)
210+
* b.c (next() object)
211+
* c (next().next() object)
203212
* </pre>
204213
*/
205214
@Override
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.core;
17+
18+
import java.util.Objects;
19+
20+
import org.jspecify.annotations.Nullable;
21+
22+
/**
23+
* Utility class for {@link PropertyPath} implementations.
24+
*
25+
* @author Mark Paluch
26+
* @since 4.1
27+
*/
28+
class PropertyPathUtil {
29+
30+
/**
31+
* Compute the hash code for the given {@link PropertyPath} based on its {@link Object#toString() string}
32+
* representation.
33+
*
34+
* @param path the property path.
35+
* @return property path hash code.
36+
*/
37+
static int hashCode(PropertyPath path) {
38+
return path.toString().hashCode();
39+
}
40+
41+
/**
42+
* Equality check for {@link PropertyPath} implementations based on their owning type and string representation.
43+
*
44+
* @param self the property path.
45+
* @param o the other object.
46+
* @return {@literal true} if both are equal; {@literal false} otherwise.
47+
*/
48+
public static boolean equals(PropertyPath self, @Nullable Object o) {
49+
50+
if (self == o) {
51+
return true;
52+
}
53+
54+
if (!(o instanceof PropertyPath that)) {
55+
return false;
56+
}
57+
58+
return Objects.equals(self.getOwningType(), that.getOwningType())
59+
&& Objects.equals(self.toString(), that.toString());
60+
}
61+
62+
}

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

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -177,45 +177,12 @@ public boolean hasNext() {
177177

178178
@Override
179179
public boolean equals(@Nullable Object o) {
180-
181-
if (this == o) {
182-
return true;
183-
}
184-
185-
if (!(o instanceof SimplePropertyPath that)) {
186-
return false;
187-
}
188-
189-
if (isCollection != that.isCollection) {
190-
return false;
191-
}
192-
193-
return Objects.equals(this.owningType, that.owningType) && Objects.equals(this.name, that.name)
194-
&& Objects.equals(this.typeInformation, that.typeInformation)
195-
&& Objects.equals(this.actualTypeInformation, that.actualTypeInformation) && Objects.equals(next, that.next);
180+
return PropertyPathUtil.equals(this, o);
196181
}
197182

198183
@Override
199184
public int hashCode() {
200-
return Objects.hash(owningType, name, typeInformation, actualTypeInformation, isCollection, next);
201-
}
202-
203-
/**
204-
* Returns the next {@link SimplePropertyPath}.
205-
*
206-
* @return the next {@link SimplePropertyPath}.
207-
* @throws IllegalStateException it there's no next one.
208-
*/
209-
private SimplePropertyPath requiredNext() {
210-
211-
SimplePropertyPath result = next;
212-
213-
if (result == null) {
214-
throw new IllegalStateException(
215-
"No next path available; Clients should call hasNext() before invoking this method");
216-
}
217-
218-
return result;
185+
return PropertyPathUtil.hashCode(this);
219186
}
220187

221188
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
* <pre class="code">
3636
* TypedPropertyPath&lt;Person, String&gt; name = TypedPropertyPath.of(Person::getName);
3737
* </pre>
38-
*
38+
*
3939
* The resulting object can be used to obtain the {@link #toDotPath() dot-path} and to interact with the targetting
4040
* property. Typed paths allow for composition to navigate nested object structures using
4141
* {@link #then(TypedPropertyPath)}:

0 commit comments

Comments
 (0)