Skip to content

Commit 24a0868

Browse files
committed
Tighten nullability contract of PersistentPropertyPath.
We should change the definition of `PersistentPropertyPath` to — in its public API — not allow empty instances anymore. Those violate the concept and bleed into the concept's API by having to make all methods nullable (returning null in exactly that "empty" case). An empty property path doesn't make any actual sense as you cannot reasonably answer the methods declared on the interface except by returning null, which then causes client code having to verify the returned values all the time. This is now changed into only making `PersistentPropertyPath.getParentPath()` nullable and letting it return null for single segment paths. Adapted client code accordingly. `….getRequiredLeadProperty()` is now deprecated in favour of `….getLeafProperty()` not returning null anymore. Fixes #2813.
1 parent 83162b2 commit 24a0868

11 files changed

+109
-114
lines changed

src/main/java/org/springframework/data/auditing/MappingAuditableBeanWrapperFactory.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ private TemporalAccessor setDateProperty(
238238

239239
property.forEach(it -> {
240240

241-
Class<?> type = it.getRequiredLeafProperty().getType();
241+
Class<?> type = it.getLeafProperty().getType();
242242

243243
this.accessor.setProperty(it, getDateValueToSet(value, type, accessor.getBean()), OPTIONS);
244244
});

src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java

+13-9
Original file line numberDiff line numberDiff line change
@@ -62,24 +62,28 @@ default void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>>
6262
Assert.notNull(path, "PersistentPropertyPath must not be null");
6363
Assert.isTrue(!path.isEmpty(), "PersistentPropertyPath must not be empty");
6464

65+
PersistentProperty<? extends PersistentProperty<?>> leafProperty = path.getLeafProperty();
6566
PersistentPropertyPath<? extends PersistentProperty<?>> parentPath = path.getParentPath();
66-
PersistentProperty<? extends PersistentProperty<?>> leafProperty = path.getRequiredLeafProperty();
67-
PersistentProperty<? extends PersistentProperty<?>> parentProperty = parentPath.isEmpty() ? null
68-
: parentPath.getLeafProperty();
6967

70-
if (parentProperty != null && (parentProperty.isCollectionLike() || parentProperty.isMap())) {
71-
throw new MappingException(
72-
String.format("Cannot traverse collection or map intermediate %s", parentPath.toDotPath()));
68+
if (parentPath != null) {
69+
70+
PersistentProperty<? extends PersistentProperty<?>> parentProperty = parentPath.getLeafProperty();
71+
72+
if (parentProperty.isCollectionLike() || parentProperty.isMap()) {
73+
throw new MappingException(
74+
"Cannot traverse collection or map intermediate %s".formatted(parentPath.toDotPath()));
75+
}
7376
}
7477

75-
Object parent = parentPath.isEmpty() ? getBean() : getProperty(parentPath);
78+
Object parent = parentPath == null ? getBean() : getProperty(parentPath);
7679

7780
if (parent == null) {
7881

7982
String nullIntermediateMessage = "Cannot lookup property %s on null intermediate; Original path was: %s on %s";
8083

8184
throw new MappingException(
82-
String.format(nullIntermediateMessage, parentProperty, path.toDotPath(), getBean().getClass().getName()));
85+
String.format(nullIntermediateMessage, path.getParentPath(), path.toDotPath(),
86+
getBean().getClass().getName()));
8387
}
8488

8589
PersistentPropertyAccessor<?> accessor = parent == getBean() //
@@ -88,7 +92,7 @@ default void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>>
8892

8993
accessor.setProperty(leafProperty, value);
9094

91-
if (parentPath.isEmpty()) {
95+
if (parentPath == null) {
9296
return;
9397
}
9498

src/main/java/org/springframework/data/mapping/PersistentPropertyPath.java

+33-25
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,25 @@ public interface PersistentPropertyPath<P extends PersistentProperty<P>> extends
3030
/**
3131
* Returns the dot based path notation using {@link PersistentProperty#getName()}.
3232
*
33-
* @return
33+
* @return will never be {@literal null}.
3434
*/
35-
@Nullable
3635
String toDotPath();
3736

3837
/**
3938
* Returns the dot based path notation using the given {@link Converter} to translate individual
4039
* {@link PersistentProperty}s to path segments.
4140
*
4241
* @param converter must not be {@literal null}.
43-
* @return
42+
* @return will never be {@literal null}.
4443
*/
45-
@Nullable
4644
String toDotPath(Converter<? super P, String> converter);
4745

4846
/**
4947
* Returns a {@link String} path with the given delimiter based on the {@link PersistentProperty#getName()}.
5048
*
51-
* @param delimiter must not be {@literal null}.
52-
* @return
49+
* @param delimiter must not be {@literal null} or empty.
50+
* @return will never be {@literal null}.
5351
*/
54-
@Nullable
5552
String toPath(String delimiter);
5653

5754
/**
@@ -60,48 +57,57 @@ public interface PersistentPropertyPath<P extends PersistentProperty<P>> extends
6057
*
6158
* @param delimiter must not be {@literal null}.
6259
* @param converter must not be {@literal null}.
63-
* @return
60+
* @return will never be {@literal null}.
6461
*/
65-
@Nullable
6662
String toPath(String delimiter, Converter<? super P, String> converter);
6763

6864
/**
6965
* Returns the last property in the {@link PersistentPropertyPath}. So for {@code foo.bar} it will return the
7066
* {@link PersistentProperty} for {@code bar}. For a simple {@code foo} it returns {@link PersistentProperty} for
7167
* {@code foo}.
7268
*
73-
* @return
69+
* @return will never be {@literal null}.
7470
*/
75-
@Nullable
7671
P getLeafProperty();
7772

73+
/**
74+
* Returns the last property in the {@link PersistentPropertyPath}. So for {@code foo.bar} it will return the
75+
* {@link PersistentProperty} for {@code bar}. For a simple {@code foo} it returns {@link PersistentProperty} for
76+
* {@code foo}.
77+
*
78+
* @return will never be {@literal null}.
79+
* @deprecated use {@link #getLeafProperty()} instead.
80+
*/
81+
@Deprecated(since = "3.1", forRemoval = true)
7882
default P getRequiredLeafProperty() {
79-
80-
P property = getLeafProperty();
81-
82-
if (property == null) {
83-
throw new IllegalStateException("No leaf property found");
84-
}
85-
86-
return property;
83+
return getLeafProperty();
8784
}
8885

8986
/**
9087
* Returns the first property in the {@link PersistentPropertyPath}. So for {@code foo.bar} it will return the
9188
* {@link PersistentProperty} for {@code foo}. For a simple {@code foo} it returns {@link PersistentProperty} for
9289
* {@code foo}.
9390
*
94-
* @return
91+
* @return will never be {@literal null}.
9592
*/
96-
@Nullable
9793
P getBaseProperty();
9894

95+
/**
96+
* Returns whether the current path is located at the root of the traversal. In other words, if the path only contains
97+
* a single property.
98+
*
99+
* @return whether the current path is located at the root of the traversal
100+
*/
101+
default boolean isRootPath() {
102+
return getLength() == 1;
103+
}
104+
99105
/**
100106
* Returns whether the given {@link PersistentPropertyPath} is a base path of the current one. This means that the
101107
* current {@link PersistentPropertyPath} is basically an extension of the given one.
102108
*
103109
* @param path must not be {@literal null}.
104-
* @return
110+
* @return whether the given {@link PersistentPropertyPath} is a base path of the current one.
105111
*/
106112
boolean isBasePathOf(PersistentPropertyPath<P> path);
107113

@@ -111,7 +117,7 @@ default P getRequiredLeafProperty() {
111117
* the current one the current {@link PersistentPropertyPath} will be returned as is.
112118
*
113119
* @param base must not be {@literal null}.
114-
* @return
120+
* @return will never be {@literal null}.
115121
*/
116122
PersistentPropertyPath<P> getExtensionForBaseOf(PersistentPropertyPath<P> base);
117123

@@ -121,13 +127,15 @@ default P getRequiredLeafProperty() {
121127
* returning the property.
122128
*
123129
* @return
130+
* @throws IllegalStateException if the current path only consists of one segment.
124131
*/
125-
PersistentPropertyPath<P> getParentPath();
132+
@Nullable
133+
PersistentPropertyPath<P> getParentPath() throws IllegalStateException;
126134

127135
/**
128136
* Returns the length of the {@link PersistentPropertyPath}.
129137
*
130-
* @return
138+
* @return a value greater than 0.
131139
*/
132140
int getLength();
133141
}

src/main/java/org/springframework/data/mapping/context/DefaultPersistentPropertyPath.java

+27-29
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
*/
3939
class DefaultPersistentPropertyPath<P extends PersistentProperty<P>> implements PersistentPropertyPath<P> {
4040

41-
private static final Converter<PersistentProperty<?>, String> DEFAULT_CONVERTER = (source) -> source.getName();
41+
private static final Converter<PersistentProperty<?>, String> DEFAULT_CONVERTER = PersistentProperty::getName;
4242
private static final String DEFAULT_DELIMITER = ".";
4343

4444
private final List<P> properties;
@@ -60,7 +60,7 @@ public DefaultPersistentPropertyPath(List<P> properties) {
6060
*
6161
* @return
6262
*/
63-
public static <T extends PersistentProperty<T>> DefaultPersistentPropertyPath<T> empty() {
63+
static <T extends PersistentProperty<T>> DefaultPersistentPropertyPath<T> empty() {
6464
return new DefaultPersistentPropertyPath<T>(Collections.emptyList());
6565
}
6666

@@ -71,15 +71,14 @@ public static <T extends PersistentProperty<T>> DefaultPersistentPropertyPath<T>
7171
* @return a new {@link DefaultPersistentPropertyPath} with the given property appended to the current one.
7272
* @throws IllegalArgumentException in case the property is not a property of the type of the current leaf property.
7373
*/
74-
public DefaultPersistentPropertyPath<P> append(P property) {
74+
DefaultPersistentPropertyPath<P> append(P property) {
7575

7676
Assert.notNull(property, "Property must not be null");
7777

7878
if (isEmpty()) {
7979
return new DefaultPersistentPropertyPath<>(Collections.singletonList(property));
8080
}
8181

82-
@SuppressWarnings("null")
8382
Class<?> leafPropertyType = getLeafProperty().getActualType();
8483

8584
Assert.isTrue(property.getOwner().getType().equals(leafPropertyType),
@@ -91,45 +90,50 @@ public DefaultPersistentPropertyPath<P> append(P property) {
9190
return new DefaultPersistentPropertyPath<>(properties);
9291
}
9392

94-
@Nullable
93+
@Override
9594
public String toDotPath() {
9695
return toPath(DEFAULT_DELIMITER, DEFAULT_CONVERTER);
9796
}
9897

99-
@Nullable
98+
@Override
10099
public String toDotPath(Converter<? super P, String> converter) {
101100
return toPath(DEFAULT_DELIMITER, converter);
102101
}
103102

104-
@Nullable
103+
@Override
105104
public String toPath(String delimiter) {
106105
return toPath(delimiter, DEFAULT_CONVERTER);
107106
}
108107

109-
@Nullable
108+
@Override
110109
public String toPath(String delimiter, Converter<? super P, String> converter) {
111110

112111
Assert.hasText(delimiter, "Delimiter must not be null or empty");
113112
Assert.notNull(converter, "Converter must not be null");
114113

115-
String result = properties.stream() //
114+
return properties.stream() //
116115
.map(converter::convert) //
117116
.filter(StringUtils::hasText) //
118117
.collect(Collectors.joining(delimiter));
119-
120-
return result.isEmpty() ? null : result;
121118
}
122119

123-
@Nullable
120+
@Override
124121
public P getLeafProperty() {
125-
return properties.isEmpty() ? null : properties.get(properties.size() - 1);
122+
123+
Assert.state(properties.size() > 0, "Empty PersistentPropertyPath should not exist");
124+
125+
return properties.get(properties.size() - 1);
126126
}
127127

128-
@Nullable
128+
@Override
129129
public P getBaseProperty() {
130-
return properties.isEmpty() ? null : properties.get(0);
130+
131+
Assert.state(properties.size() > 0, "Empty PersistentPropertyPath should not exist");
132+
133+
return properties.get(0);
131134
}
132135

136+
@Override
133137
public boolean isBasePathOf(PersistentPropertyPath<P> path) {
134138

135139
Assert.notNull(path, "PersistentPropertyPath must not be null");
@@ -152,37 +156,31 @@ public boolean isBasePathOf(PersistentPropertyPath<P> path) {
152156
return true;
153157
}
154158

159+
@Override
155160
public PersistentPropertyPath<P> getExtensionForBaseOf(PersistentPropertyPath<P> base) {
156161

157162
if (!base.isBasePathOf(this)) {
158163
return this;
159164
}
160165

161-
List<P> result = new ArrayList<>();
162-
Iterator<P> iterator = iterator();
163-
164-
for (int i = 0; i < base.getLength(); i++) {
165-
iterator.next();
166-
}
167-
168-
while (iterator.hasNext()) {
169-
result.add(iterator.next());
170-
}
171-
172-
return new DefaultPersistentPropertyPath<>(result);
166+
return new DefaultPersistentPropertyPath<>(properties.subList(base.getLength(), getLength()));
173167
}
174168

169+
@Nullable
170+
@Override
175171
public PersistentPropertyPath<P> getParentPath() {
176172

177173
int size = properties.size();
178174

179-
return size == 0 ? this : new DefaultPersistentPropertyPath<>(properties.subList(0, size - 1));
175+
return size == 1 ? null : new DefaultPersistentPropertyPath<>(properties.subList(0, size - 1));
180176
}
181177

178+
@Override
182179
public int getLength() {
183180
return properties.size();
184181
}
185182

183+
@Override
186184
public Iterator<P> iterator() {
187185
return properties.iterator();
188186
}
@@ -202,7 +200,7 @@ public boolean containsPropertyOfType(@Nullable TypeInformation<?> type) {
202200
}
203201

204202
@Override
205-
public boolean equals(Object o) {
203+
public boolean equals(@Nullable Object o) {
206204

207205
if (this == o) {
208206
return true;

src/main/java/org/springframework/data/mapping/context/InvalidPersistentPropertyPath.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public class InvalidPersistentPropertyPath extends MappingException {
5454
public InvalidPersistentPropertyPath(String source, TypeInformation<?> type, String unresolvableSegment,
5555
PersistentPropertyPath<? extends PersistentProperty<?>> resolvedPath) {
5656

57-
super(createMessage(resolvedPath.isEmpty() ? type : resolvedPath.getRequiredLeafProperty().getTypeInformation(),
57+
super(createMessage(resolvedPath.isEmpty() ? type : resolvedPath.getLeafProperty().getTypeInformation(),
5858
unresolvableSegment));
5959

6060
Assert.notNull(source, "Source property path must not be null");

0 commit comments

Comments
 (0)