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.
This commit is contained in:
Oliver Drotbohm
2023-03-01 22:10:02 +01:00
parent 83162b25c7
commit 35a2f45736
12 changed files with 115 additions and 121 deletions

View File

@@ -238,7 +238,7 @@ public class MappingAuditableBeanWrapperFactory extends DefaultAuditableBeanWrap
property.forEach(it -> {
Class<?> type = it.getRequiredLeafProperty().getType();
Class<?> type = it.getLeafProperty().getType();
this.accessor.setProperty(it, getDateValueToSet(value, type, accessor.getBean()), OPTIONS);
});

View File

@@ -62,24 +62,28 @@ public interface PersistentPropertyAccessor<T> {
Assert.notNull(path, "PersistentPropertyPath must not be null");
Assert.isTrue(!path.isEmpty(), "PersistentPropertyPath must not be empty");
PersistentProperty<? extends PersistentProperty<?>> leafProperty = path.getLeafProperty();
PersistentPropertyPath<? extends PersistentProperty<?>> parentPath = path.getParentPath();
PersistentProperty<? extends PersistentProperty<?>> leafProperty = path.getRequiredLeafProperty();
PersistentProperty<? extends PersistentProperty<?>> parentProperty = parentPath.isEmpty() ? null
: parentPath.getLeafProperty();
if (parentProperty != null && (parentProperty.isCollectionLike() || parentProperty.isMap())) {
throw new MappingException(
String.format("Cannot traverse collection or map intermediate %s", parentPath.toDotPath()));
if (parentPath != null) {
PersistentProperty<? extends PersistentProperty<?>> parentProperty = parentPath.getLeafProperty();
if (parentProperty.isCollectionLike() || parentProperty.isMap()) {
throw new MappingException(
"Cannot traverse collection or map intermediate %s".formatted(parentPath.toDotPath()));
}
}
Object parent = parentPath.isEmpty() ? getBean() : getProperty(parentPath);
Object parent = parentPath == null ? getBean() : getProperty(parentPath);
if (parent == null) {
String nullIntermediateMessage = "Cannot lookup property %s on null intermediate; Original path was: %s on %s";
throw new MappingException(
String.format(nullIntermediateMessage, parentProperty, path.toDotPath(), getBean().getClass().getName()));
String.format(nullIntermediateMessage, path.getParentPath(), path.toDotPath(),
getBean().getClass().getName()));
}
PersistentPropertyAccessor<?> accessor = parent == getBean() //
@@ -88,7 +92,7 @@ public interface PersistentPropertyAccessor<T> {
accessor.setProperty(leafProperty, value);
if (parentPath.isEmpty()) {
if (parentPath == null) {
return;
}

View File

@@ -30,9 +30,8 @@ public interface PersistentPropertyPath<P extends PersistentProperty<P>> extends
/**
* Returns the dot based path notation using {@link PersistentProperty#getName()}.
*
* @return
* @return will never be {@literal null}.
*/
@Nullable
String toDotPath();
/**
@@ -40,18 +39,16 @@ public interface PersistentPropertyPath<P extends PersistentProperty<P>> extends
* {@link PersistentProperty}s to path segments.
*
* @param converter must not be {@literal null}.
* @return
* @return will never be {@literal null}.
*/
@Nullable
String toDotPath(Converter<? super P, String> converter);
/**
* Returns a {@link String} path with the given delimiter based on the {@link PersistentProperty#getName()}.
*
* @param delimiter must not be {@literal null}.
* @return
* @param delimiter must not be {@literal null} or empty.
* @return will never be {@literal null}.
*/
@Nullable
String toPath(String delimiter);
/**
@@ -60,9 +57,8 @@ public interface PersistentPropertyPath<P extends PersistentProperty<P>> extends
*
* @param delimiter must not be {@literal null}.
* @param converter must not be {@literal null}.
* @return
* @return will never be {@literal null}.
*/
@Nullable
String toPath(String delimiter, Converter<? super P, String> converter);
/**
@@ -70,20 +66,21 @@ public interface PersistentPropertyPath<P extends PersistentProperty<P>> extends
* {@link PersistentProperty} for {@code bar}. For a simple {@code foo} it returns {@link PersistentProperty} for
* {@code foo}.
*
* @return
* @return will never be {@literal null}.
*/
@Nullable
P getLeafProperty();
/**
* Returns the last property in the {@link PersistentPropertyPath}. So for {@code foo.bar} it will return the
* {@link PersistentProperty} for {@code bar}. For a simple {@code foo} it returns {@link PersistentProperty} for
* {@code foo}.
*
* @return will never be {@literal null}.
* @deprecated use {@link #getLeafProperty()} instead.
*/
@Deprecated(since = "3.1", forRemoval = true)
default P getRequiredLeafProperty() {
P property = getLeafProperty();
if (property == null) {
throw new IllegalStateException("No leaf property found");
}
return property;
return getLeafProperty();
}
/**
@@ -91,17 +88,26 @@ public interface PersistentPropertyPath<P extends PersistentProperty<P>> extends
* {@link PersistentProperty} for {@code foo}. For a simple {@code foo} it returns {@link PersistentProperty} for
* {@code foo}.
*
* @return
* @return will never be {@literal null}.
*/
@Nullable
P getBaseProperty();
/**
* Returns whether the current path is located at the root of the traversal. In other words, if the path only contains
* a single property.
*
* @return whether the current path is located at the root of the traversal
*/
default boolean isRootPath() {
return getLength() == 1;
}
/**
* Returns whether the given {@link PersistentPropertyPath} is a base path of the current one. This means that the
* current {@link PersistentPropertyPath} is basically an extension of the given one.
*
* @param path must not be {@literal null}.
* @return
* @return whether the given {@link PersistentPropertyPath} is a base path of the current one.
*/
boolean isBasePathOf(PersistentPropertyPath<P> path);
@@ -111,7 +117,7 @@ public interface PersistentPropertyPath<P extends PersistentProperty<P>> extends
* the current one the current {@link PersistentPropertyPath} will be returned as is.
*
* @param base must not be {@literal null}.
* @return
* @return will never be {@literal null}.
*/
PersistentPropertyPath<P> getExtensionForBaseOf(PersistentPropertyPath<P> base);
@@ -121,13 +127,15 @@ public interface PersistentPropertyPath<P extends PersistentProperty<P>> extends
* returning the property.
*
* @return
* @throws IllegalStateException if the current path only consists of one segment.
*/
PersistentPropertyPath<P> getParentPath();
@Nullable
PersistentPropertyPath<P> getParentPath() throws IllegalStateException;
/**
* Returns the length of the {@link PersistentPropertyPath}.
*
* @return
* @return a value greater than 0.
*/
int getLength();
}

View File

@@ -44,20 +44,19 @@ public interface PersistentPropertyPathAccessor<T> extends PersistentPropertyAcc
/**
* Return the value pointed to by the given {@link PersistentPropertyPath}. If the given path is empty, the wrapped
* bean is returned. On each path segment value lookup, the resulting value is post-processed by handlers registered
* on the given {@link TraversalContext} context. This can be used to unwrap container types that are encountered
* during the traversal.
* bean is returned. On each path segment value lookup, the resulting value is post-processed depending on the given
* {@link GetOptions}.
*
* @param path must not be {@literal null}.
* @param context must not be {@literal null}.
* @param options must not be {@literal null}.
* @return
*/
@Nullable
Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, GetOptions context);
Object getProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, GetOptions options);
/**
* Sets the given value for the {@link PersistentProperty} pointed to by the given {@link PersistentPropertyPath}. The
* lookup of intermediate values must not yield {@literal null}.
* Sets the given value for the {@link PersistentProperty} pointed to by the given {@link PersistentPropertyPath}.
* The lookup of intermediate values must not yield {@literal null}.
*
* @param path must not be {@literal null} or empty.
* @param value can be {@literal null}.

View File

@@ -38,7 +38,7 @@ import org.springframework.util.StringUtils;
*/
class DefaultPersistentPropertyPath<P extends PersistentProperty<P>> implements PersistentPropertyPath<P> {
private static final Converter<PersistentProperty<?>, String> DEFAULT_CONVERTER = (source) -> source.getName();
private static final Converter<PersistentProperty<?>, String> DEFAULT_CONVERTER = PersistentProperty::getName;
private static final String DEFAULT_DELIMITER = ".";
private final List<P> properties;
@@ -60,7 +60,7 @@ class DefaultPersistentPropertyPath<P extends PersistentProperty<P>> implements
*
* @return
*/
public static <T extends PersistentProperty<T>> DefaultPersistentPropertyPath<T> empty() {
static <T extends PersistentProperty<T>> DefaultPersistentPropertyPath<T> empty() {
return new DefaultPersistentPropertyPath<T>(Collections.emptyList());
}
@@ -71,7 +71,7 @@ class DefaultPersistentPropertyPath<P extends PersistentProperty<P>> implements
* @return a new {@link DefaultPersistentPropertyPath} with the given property appended to the current one.
* @throws IllegalArgumentException in case the property is not a property of the type of the current leaf property.
*/
public DefaultPersistentPropertyPath<P> append(P property) {
DefaultPersistentPropertyPath<P> append(P property) {
Assert.notNull(property, "Property must not be null");
@@ -79,7 +79,6 @@ class DefaultPersistentPropertyPath<P extends PersistentProperty<P>> implements
return new DefaultPersistentPropertyPath<>(Collections.singletonList(property));
}
@SuppressWarnings("null")
Class<?> leafPropertyType = getLeafProperty().getActualType();
Assert.isTrue(property.getOwner().getType().equals(leafPropertyType),
@@ -91,45 +90,50 @@ class DefaultPersistentPropertyPath<P extends PersistentProperty<P>> implements
return new DefaultPersistentPropertyPath<>(properties);
}
@Nullable
@Override
public String toDotPath() {
return toPath(DEFAULT_DELIMITER, DEFAULT_CONVERTER);
}
@Nullable
@Override
public String toDotPath(Converter<? super P, String> converter) {
return toPath(DEFAULT_DELIMITER, converter);
}
@Nullable
@Override
public String toPath(String delimiter) {
return toPath(delimiter, DEFAULT_CONVERTER);
}
@Nullable
@Override
public String toPath(String delimiter, Converter<? super P, String> converter) {
Assert.hasText(delimiter, "Delimiter must not be null or empty");
Assert.notNull(converter, "Converter must not be null");
String result = properties.stream() //
return properties.stream() //
.map(converter::convert) //
.filter(StringUtils::hasText) //
.collect(Collectors.joining(delimiter));
return result.isEmpty() ? null : result;
}
@Nullable
@Override
public P getLeafProperty() {
return properties.isEmpty() ? null : properties.get(properties.size() - 1);
Assert.state(properties.size() > 0, "Empty PersistentPropertyPath should not exist");
return properties.get(properties.size() - 1);
}
@Nullable
@Override
public P getBaseProperty() {
return properties.isEmpty() ? null : properties.get(0);
Assert.state(properties.size() > 0, "Empty PersistentPropertyPath should not exist");
return properties.get(0);
}
@Override
public boolean isBasePathOf(PersistentPropertyPath<P> path) {
Assert.notNull(path, "PersistentPropertyPath must not be null");
@@ -152,37 +156,31 @@ class DefaultPersistentPropertyPath<P extends PersistentProperty<P>> implements
return true;
}
@Override
public PersistentPropertyPath<P> getExtensionForBaseOf(PersistentPropertyPath<P> base) {
if (!base.isBasePathOf(this)) {
return this;
}
List<P> result = new ArrayList<>();
Iterator<P> iterator = iterator();
for (int i = 0; i < base.getLength(); i++) {
iterator.next();
}
while (iterator.hasNext()) {
result.add(iterator.next());
}
return new DefaultPersistentPropertyPath<>(result);
return new DefaultPersistentPropertyPath<>(properties.subList(base.getLength(), getLength()));
}
@Nullable
@Override
public PersistentPropertyPath<P> getParentPath() {
int size = properties.size();
return size == 0 ? this : new DefaultPersistentPropertyPath<>(properties.subList(0, size - 1));
return size == 1 ? null : new DefaultPersistentPropertyPath<>(properties.subList(0, size - 1));
}
@Override
public int getLength() {
return properties.size();
}
@Override
public Iterator<P> iterator() {
return properties.iterator();
}
@@ -202,7 +200,7 @@ class DefaultPersistentPropertyPath<P extends PersistentProperty<P>> implements
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;

View File

@@ -54,7 +54,7 @@ public class InvalidPersistentPropertyPath extends MappingException {
public InvalidPersistentPropertyPath(String source, TypeInformation<?> type, String unresolvableSegment,
PersistentPropertyPath<? extends PersistentProperty<?>> resolvedPath) {
super(createMessage(resolvedPath.isEmpty() ? type : resolvedPath.getRequiredLeafProperty().getTypeInformation(),
super(createMessage(resolvedPath.isEmpty() ? type : resolvedPath.getLeafProperty().getTypeInformation(),
unresolvableSegment));
Assert.notNull(source, "Source property path must not be null");

View File

@@ -59,13 +59,13 @@ class PersistentPropertyPathFactory<E extends PersistentEntity<?, P>, P extends
* Creates a new {@link PersistentPropertyPath} for the given property path on the given type.
*
* @param type must not be {@literal null}.
* @param propertyPath must not be {@literal null}.
* @param propertyPath must not be {@literal null} or empty.
* @return
*/
public PersistentPropertyPath<P> from(Class<?> type, String propertyPath) {
Assert.notNull(type, "Type must not be null");
Assert.notNull(propertyPath, "Property path must not be null");
Assert.hasText(propertyPath, "Property path must not be null or empty");
return getPersistentPropertyPath(TypeInformation.of(type), propertyPath);
}
@@ -74,13 +74,13 @@ class PersistentPropertyPathFactory<E extends PersistentEntity<?, P>, P extends
* Creates a new {@link PersistentPropertyPath} for the given property path on the given type.
*
* @param type must not be {@literal null}.
* @param propertyPath must not be {@literal null}.
* @param propertyPath must not be {@literal null} or empty.
* @return
*/
public PersistentPropertyPath<P> from(TypeInformation<?> type, String propertyPath) {
Assert.notNull(type, "Type must not be null");
Assert.notNull(propertyPath, "Property path must not be null");
Assert.hasText(propertyPath, "Property path must not be null or empty");
return getPersistentPropertyPath(type, propertyPath);
}
@@ -181,10 +181,9 @@ class PersistentPropertyPathFactory<E extends PersistentEntity<?, P>, P extends
private PersistentPropertyPath<P> createPersistentPropertyPath(String propertyPath, TypeInformation<?> type) {
String trimmedPath = propertyPath.trim();
List<String> parts = trimmedPath.isEmpty() ? Collections.emptyList() : List.of(trimmedPath.split("\\."));
List<String> parts = trimmedPath.isEmpty() //
? Collections.emptyList() //
: Arrays.asList(trimmedPath.split("\\."));
Assert.notEmpty(parts, "Cannot create PersistentPropertyPath from empty segments");
DefaultPersistentPropertyPath<P> path = DefaultPersistentPropertyPath.empty();
Iterator<String> iterator = parts.iterator();
@@ -193,8 +192,7 @@ class PersistentPropertyPathFactory<E extends PersistentEntity<?, P>, P extends
while (iterator.hasNext()) {
String segment = iterator.next();
final DefaultPersistentPropertyPath<P> currentPath = path;
DefaultPersistentPropertyPath<P> currentPath = path;
Pair<DefaultPersistentPropertyPath<P>, E> pair = getPair(path, iterator, segment, current);
if (pair == null) {
@@ -233,8 +231,7 @@ class PersistentPropertyPathFactory<E extends PersistentEntity<?, P>, P extends
return Collections.emptyList();
}
E entity = context.getRequiredPersistentEntity(actualType);
return from(entity, filter, traversalGuard, basePath);
return from(context.getRequiredPersistentEntity(actualType), filter, traversalGuard, basePath);
}
private Collection<PersistentPropertyPath<P>> from(E entity, Predicate<? super P> filter, Predicate<P> traversalGuard,
@@ -258,7 +255,8 @@ class PersistentPropertyPathFactory<E extends PersistentEntity<?, P>, P extends
}
if (traversalGuard.and(IS_ENTITY).test(persistentProperty)) {
properties.addAll(from(context.getPersistentEntity(persistentProperty), filter, traversalGuard, currentPath));
var persistentEntity = context.getRequiredPersistentEntity(persistentProperty);
properties.addAll(from(persistentEntity, filter, traversalGuard, currentPath));
}
};
@@ -294,7 +292,7 @@ class PersistentPropertyPathFactory<E extends PersistentEntity<?, P>, P extends
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;

View File

@@ -63,7 +63,7 @@ public class ConvertingPropertyAccessor<T> extends SimplePersistentPropertyPathA
@Override
public void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, @Nullable Object value) {
Object converted = convertIfNecessary(value, path.getRequiredLeafProperty().getType());
Object converted = convertIfNecessary(value, path.getLeafProperty().getType());
super.setProperty(path, converted);
}

View File

@@ -24,7 +24,6 @@ import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.CollectionFactory;
import org.springframework.data.mapping.AccessOptions;
import org.springframework.data.mapping.AccessOptions.GetOptions;
@@ -114,8 +113,8 @@ class SimplePersistentPropertyPathAccessor<T> implements PersistentPropertyPathA
setProperty(path, value, AccessOptions.defaultSetOptions());
}
@SuppressWarnings("unchecked")
@Override
@SuppressWarnings("unchecked")
public void setProperty(PersistentPropertyPath<? extends PersistentProperty<?>> path, @Nullable Object value,
SetOptions options) {
@@ -123,7 +122,13 @@ class SimplePersistentPropertyPathAccessor<T> implements PersistentPropertyPathA
Assert.isTrue(!path.isEmpty(), "PersistentPropertyPath must not be empty");
PersistentPropertyPath<? extends PersistentProperty<?>> parentPath = path.getParentPath();
PersistentProperty<? extends PersistentProperty<?>> leafProperty = path.getRequiredLeafProperty();
if (parentPath == null) {
setProperty(path.getLeafProperty(), value);
return;
}
PersistentProperty<? extends PersistentProperty<?>> leafProperty = path.getLeafProperty();
if (!options.propagate(parentPath.getLeafProperty())) {
return;
@@ -133,7 +138,7 @@ class SimplePersistentPropertyPathAccessor<T> implements PersistentPropertyPathA
? DEFAULT_GET_OPTIONS.withNullValues(GetNulls.EARLY_RETURN)
: DEFAULT_GET_OPTIONS;
Object parent = parentPath.isEmpty() ? getBean() : getProperty(parentPath, lookupOptions);
Object parent = getProperty(parentPath, lookupOptions);
if (parent == null) {
handleNull(path, options.getNullHandling());
@@ -146,7 +151,7 @@ class SimplePersistentPropertyPathAccessor<T> implements PersistentPropertyPathA
return;
}
PersistentProperty<? extends PersistentProperty<?>> parentProperty = parentPath.getRequiredLeafProperty();
PersistentProperty<? extends PersistentProperty<?>> parentProperty = parentPath.getLeafProperty();
Object newValue;
@@ -193,6 +198,7 @@ class SimplePersistentPropertyPathAccessor<T> implements PersistentPropertyPathA
* @return
*/
@Nullable
@SuppressWarnings("null")
private Object handleNull(PersistentPropertyPath<? extends PersistentProperty<?>> path, SetNulls handling) {
if (SKIP.equals(handling)) {