diff --git a/src/main/java/org/springframework/data/mapping/PersistentProperty.java b/src/main/java/org/springframework/data/mapping/PersistentProperty.java
index 3d929deb8..cc5be87f5 100644
--- a/src/main/java/org/springframework/data/mapping/PersistentProperty.java
+++ b/src/main/java/org/springframework/data/mapping/PersistentProperty.java
@@ -21,9 +21,11 @@ import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
+import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
/**
* @author Graeme Rocher
@@ -62,16 +64,6 @@ public interface PersistentProperty
> {
*/
TypeInformation> getTypeInformation();
- /**
- * Returns the {@link TypeInformation} if the property references a {@link PersistentEntity}. Will return
- * {@literal null} in case it refers to a simple type. Will return {@link Collection}'s component type or the
- * {@link Map}'s value type transparently.
- *
- * @deprecated Use getPersistentEntityTypes instead.
- */
- @Deprecated
- Iterable extends TypeInformation>> getPersistentEntityType();
-
/**
* Returns the {@link TypeInformation} if the property references a {@link PersistentEntity}. Will return
* {@literal null} in case it refers to a simple type. Will return {@link Collection}'s component type or the
@@ -79,9 +71,7 @@ public interface PersistentProperty
> {
*
* @return
*/
- default Iterable extends TypeInformation>> getPersistentEntityTypes() {
- return getPersistentEntityType();
- };
+ Iterable extends TypeInformation>> getPersistentEntityTypes();
/**
* Returns the getter method to access the property value if available. Might return {@literal null} in case there is
@@ -328,4 +318,19 @@ public interface PersistentProperty
> {
* @return
*/
boolean usePropertyAccess();
+
+ /**
+ * Returns whether the actual type of the property carries the given annotation.
+ *
+ * @param annotationType must not be {@literal null}.
+ * @return
+ * @since 2.1
+ * @see #getActualType()
+ */
+ default boolean hasActualTypeAnnotation(Class extends Annotation> annotationType) {
+
+ Assert.notNull(annotationType, "Annotation type must not be null!");
+
+ return AnnotatedElementUtils.hasAnnotation(getActualType(), annotationType);
+ }
}
diff --git a/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java b/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java
index b9a14b3bc..85ed388c9 100644
--- a/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java
+++ b/src/main/java/org/springframework/data/mapping/PersistentPropertyAccessor.java
@@ -17,6 +17,7 @@ package org.springframework.data.mapping;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
/**
* Domain service to allow accessing and setting {@link PersistentProperty}s of an entity. Usually obtained through
@@ -40,6 +41,37 @@ public interface PersistentPropertyAccessor {
*/
void setProperty(PersistentProperty> property, @Nullable Object value);
+ /**
+ * 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}.
+ * @since 2.1
+ */
+ default void setProperty(PersistentPropertyPath extends PersistentProperty>> path, @Nullable Object value) {
+
+ Assert.notNull(path, "PersistentPropertyPath must not be null!");
+ Assert.isTrue(!path.isEmpty(), "PersistentPropertyPath must not be empty!");
+
+ PersistentPropertyPath extends PersistentProperty>> parentPath = path.getParentPath();
+ PersistentProperty> leafProperty = path.getRequiredLeafProperty();
+
+ Object parent = parentPath.isEmpty() ? 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, parentPath.getLeafProperty(), path.toDotPath(),
+ getBean().getClass().getName()));
+ }
+
+ PersistentPropertyAccessor accessor = leafProperty.getOwner().getPropertyAccessor(parent);
+
+ accessor.setProperty(leafProperty, value);
+ }
+
/**
* Returns the value of the given {@link PersistentProperty} of the underlying bean instance.
*
@@ -49,6 +81,43 @@ public interface PersistentPropertyAccessor {
@Nullable
Object getProperty(PersistentProperty> property);
+ /**
+ * Return the value pointed to by the given {@link PersistentPropertyPath}. If the given path is empty, the wrapped
+ * bean is returned.
+ *
+ * @param path must not be {@literal null}.
+ * @return
+ * @since 2.1
+ */
+ @Nullable
+ default Object getProperty(PersistentPropertyPath extends PersistentProperty>> path) {
+
+ Object bean = getBean();
+ Object current = bean;
+
+ if (path.isEmpty()) {
+ return bean;
+ }
+
+ for (PersistentProperty> property : path) {
+
+ if (current == null) {
+
+ String nullIntermediateMessage = "Cannot lookup property %s on null intermediate! Original path was: %s on %s.";
+
+ throw new MappingException(
+ String.format(nullIntermediateMessage, property, path.toDotPath(), bean.getClass().getName()));
+ }
+
+ PersistentEntity, ? extends PersistentProperty>> entity = property.getOwner();
+ PersistentPropertyAccessor accessor = entity.getPropertyAccessor(current);
+
+ current = accessor.getProperty(property);
+ }
+
+ return current;
+ }
+
/**
* Returns the underlying bean.
*
diff --git a/src/main/java/org/springframework/data/mapping/context/PersistentPropertyPath.java b/src/main/java/org/springframework/data/mapping/PersistentPropertyPath.java
similarity index 82%
rename from src/main/java/org/springframework/data/mapping/context/PersistentPropertyPath.java
rename to src/main/java/org/springframework/data/mapping/PersistentPropertyPath.java
index 6dc39894f..ef3eb8c12 100644
--- a/src/main/java/org/springframework/data/mapping/context/PersistentPropertyPath.java
+++ b/src/main/java/org/springframework/data/mapping/PersistentPropertyPath.java
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.data.mapping.context;
+package org.springframework.data.mapping;
import org.springframework.core.convert.converter.Converter;
-import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
/**
@@ -24,7 +24,7 @@ import org.springframework.lang.Nullable;
*
* @author Oliver Gierke
*/
-public interface PersistentPropertyPath> extends Iterable {
+public interface PersistentPropertyPath> extends Streamable
{
/**
* Returns the dot based path notation using {@link PersistentProperty#getName()}.
@@ -42,7 +42,7 @@ public interface PersistentPropertyPath> extends
* @return
*/
@Nullable
- String toDotPath(Converter super T, String> converter);
+ String toDotPath(Converter super P, String> converter);
/**
* Returns a {@link String} path with the given delimiter based on the {@link PersistentProperty#getName()}.
@@ -62,7 +62,7 @@ public interface PersistentPropertyPath> extends
* @return
*/
@Nullable
- String toPath(String delimiter, Converter super T, String> converter);
+ String toPath(String delimiter, Converter super P, String> converter);
/**
* Returns the last property in the {@link PersistentPropertyPath}. So for {@code foo.bar} it will return the
@@ -72,7 +72,18 @@ public interface PersistentPropertyPath> extends
* @return
*/
@Nullable
- T getLeafProperty();
+ P getLeafProperty();
+
+ default P getRequiredLeafProperty() {
+
+ P property = getLeafProperty();
+
+ if (property == null) {
+ throw new IllegalStateException("No leaf property found!");
+ }
+
+ return property;
+ }
/**
* Returns the first property in the {@link PersistentPropertyPath}. So for {@code foo.bar} it will return the
@@ -82,7 +93,7 @@ public interface PersistentPropertyPath> extends
* @return
*/
@Nullable
- T getBaseProperty();
+ P getBaseProperty();
/**
* Returns whether the given {@link PersistentPropertyPath} is a base path of the current one. This means that the
@@ -91,7 +102,7 @@ public interface PersistentPropertyPath> extends
* @param path must not be {@literal null}.
* @return
*/
- boolean isBasePathOf(PersistentPropertyPath path);
+ boolean isBasePathOf(PersistentPropertyPath path);
/**
* Returns the sub-path of the current one as if it was based on the given base path. So for a current path
@@ -101,7 +112,7 @@ public interface PersistentPropertyPath> extends
* @param base must not be {@literal null}.
* @return
*/
- PersistentPropertyPath getExtensionForBaseOf(PersistentPropertyPath base);
+ PersistentPropertyPath getExtensionForBaseOf(PersistentPropertyPath
base);
/**
* Returns the parent path of the current {@link PersistentPropertyPath}, i.e. the path without the leaf property.
@@ -110,7 +121,7 @@ public interface PersistentPropertyPath> extends
*
* @return
*/
- PersistentPropertyPath getParentPath();
+ PersistentPropertyPath getParentPath();
/**
* Returns the length of the {@link PersistentPropertyPath}.
@@ -118,12 +129,4 @@ public interface PersistentPropertyPath> extends
* @return
*/
int getLength();
-
- /**
- * Returns whether the path is empty.
- *
- * @return
- * @since 1.11
- */
- boolean isEmpty();
}
diff --git a/src/main/java/org/springframework/data/mapping/PersistentPropertyPaths.java b/src/main/java/org/springframework/data/mapping/PersistentPropertyPaths.java
new file mode 100644
index 000000000..02139305c
--- /dev/null
+++ b/src/main/java/org/springframework/data/mapping/PersistentPropertyPaths.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mapping;
+
+import java.util.Optional;
+
+import org.springframework.data.util.Streamable;
+
+/**
+ * A wrapper for a collection of {@link PersistentPropertyPath}s.
+ *
+ * @author Oliver Gierke
+ * @since 2.1
+ * @soundtrack Stuart McCallum - North Star (City)
+ */
+public interface PersistentPropertyPaths>
+ extends Streamable> {
+
+ /**
+ * Returns the first {@link PersistentPropertyPath}.
+ *
+ * @return
+ */
+ Optional> getFirst();
+
+ /**
+ * Returns whether the given path is contained in the current {@link PersistentPropertyPaths}.
+ *
+ * @param path must not be {@literal null}.
+ * @return
+ */
+ boolean contains(String path);
+
+ /**
+ * Returns whether the given {@link PropertyPath} is contained in the current {@link PersistentPropertyPaths}.
+ *
+ * @param path must not be {@literal null}.
+ * @return
+ */
+ boolean contains(PropertyPath path);
+}
diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
index a53e70b46..fc39cca9e 100644
--- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
+++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java
@@ -18,23 +18,20 @@ package org.springframework.data.mapping.context;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
-import lombok.Value;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.springframework.beans.BeanUtils;
@@ -45,6 +42,8 @@ import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory;
import org.springframework.data.mapping.model.MutablePersistentEntity;
@@ -53,16 +52,13 @@ import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Optionals;
-import org.springframework.data.util.Pair;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
-import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
import org.springframework.util.ReflectionUtils.FieldFilter;
-import org.springframework.util.StringUtils;
/**
* Base class to build mapping metadata and thus create instances of {@link PersistentEntity} and
@@ -87,8 +83,8 @@ public abstract class AbstractMappingContext NONE = Optional.empty();
private final Map, Optional> persistentEntities = new HashMap<>();
- private final Map> propertyPaths = new ConcurrentReferenceHashMap<>();
private final PersistentPropertyAccessorFactory persistentPropertyAccessorFactory = new ClassGeneratingPropertyAccessorFactory();
+ private final PersistentPropertyPathFactory persistentPropertyPathFactory;
private @Nullable ApplicationEventPublisher applicationEventPublisher;
@@ -100,6 +96,10 @@ public abstract class AbstractMappingContext(this);
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher)
@@ -253,10 +253,7 @@ public abstract class AbstractMappingContext getPersistentPropertyPath(PropertyPath propertyPath) {
-
- Assert.notNull(propertyPath, "Property path must not be null!");
-
- return getPersistentPropertyPath(propertyPath.toDotPath(), propertyPath.getOwningType());
+ return persistentPropertyPathFactory.from(propertyPath);
}
/*
@@ -265,11 +262,7 @@ public abstract class AbstractMappingContext getPersistentPropertyPath(String propertyPath, Class> type) {
-
- Assert.notNull(propertyPath, "Property path must not be null!");
- Assert.notNull(type, "Type must not be null!");
-
- return getPersistentPropertyPath(propertyPath, ClassTypeInformation.from(type));
+ return persistentPropertyPathFactory.from(type, propertyPath);
}
/*
@@ -278,65 +271,36 @@ public abstract class AbstractMappingContext getPersistentPropertyPath(InvalidPersistentPropertyPath invalidPath) {
- return getPersistentPropertyPath(invalidPath.getResolvedPath(), invalidPath.getType());
+ return persistentPropertyPathFactory.from(invalidPath.getType(), invalidPath.getResolvedPath());
}
- private PersistentPropertyPath getPersistentPropertyPath(String propertyPath, TypeInformation> type) {
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mapping.context.MappingContext#findPersistentPropertyPath(java.lang.Class, java.util.function.Predicate)
+ */
+ @Override
+ public PersistentPropertyPaths findPersistentPropertyPaths(Class type, Predicate super P> predicate) {
- return propertyPaths.computeIfAbsent(TypeAndProperties.of(type, propertyPath),
- it -> createPersistentPropertyPath(it.getPath(), it.getType()));
+ Assert.notNull(type, "Type must not be null!");
+ Assert.notNull(predicate, "Selection predicate must not be null!");
+
+ return doFindPersistentPropertyPaths(type, predicate, it -> !it.isAssociation());
}
/**
- * Creates a {@link PersistentPropertyPath} for the given parts and {@link TypeInformation}.
- *
- * @param propertyPath must not be {@literal null}.
- * @param type must not be {@literal null}.
- * @return
+ * Actually looks up the {@link PersistentPropertyPaths} for the given type, selection predicate and traversal guard.
+ * Primary purpose is to allow sub-types to alter the default traversal guard, e.g. used by
+ * {@link #findPersistentPropertyPaths(Class, Predicate)}.
+ *
+ * @param type will never be {@literal null}.
+ * @param predicate will never be {@literal null}.
+ * @param traversalGuard will never be {@literal null}.
+ * @return will never be {@literal null}.
+ * @see #findPersistentPropertyPaths(Class, Predicate)
*/
- private PersistentPropertyPath createPersistentPropertyPath(String propertyPath, TypeInformation> type) {
-
- List parts = Arrays.asList(propertyPath.split("\\."));
- DefaultPersistentPropertyPath path = DefaultPersistentPropertyPath.empty();
- Iterator iterator = parts.iterator();
- E current = getRequiredPersistentEntity(type);
-
- while (iterator.hasNext()) {
-
- String segment = iterator.next();
- final DefaultPersistentPropertyPath foo = path;
- final E bar = current;
-
- Pair, E> pair = getPair(path, iterator, segment, current);
-
- if (pair == null) {
-
- String source = StringUtils.collectionToDelimitedString(parts, ".");
- String resolvedPath = foo.toDotPath();
-
- throw new InvalidPersistentPropertyPath(source, type, segment, resolvedPath,
- String.format("No property %s found on %s!", segment, bar.getName()));
- }
-
- path = pair.getFirst();
- current = pair.getSecond();
- }
-
- return path;
- }
-
- @Nullable
- private Pair, E> getPair(DefaultPersistentPropertyPath path,
- Iterator iterator, String segment, E entity) {
-
- P property = entity.getPersistentProperty(segment);
-
- if (property == null) {
- return null;
- }
-
- TypeInformation> type = property.getTypeInformation().getRequiredActualType();
- return Pair.of(path.append(property), iterator.hasNext() ? getRequiredPersistentEntity(type) : entity);
+ protected final PersistentPropertyPaths doFindPersistentPropertyPaths(Class type,
+ Predicate super P> predicate, Predicate traversalGuard) {
+ return persistentPropertyPathFactory.from(ClassTypeInformation.from(type), predicate, traversalGuard);
}
/**
@@ -573,13 +537,6 @@ public abstract class AbstractMappingContext type;
- String path;
- }
-
/**
* Filter rejecting static fields as well as artificially introduced ones. See
* {@link PersistentPropertyFilter#UNMAPPED_PROPERTIES} for details.
diff --git a/src/main/java/org/springframework/data/mapping/context/DefaultPersistentPropertyPath.java b/src/main/java/org/springframework/data/mapping/context/DefaultPersistentPropertyPath.java
index 375c50aef..841966fd8 100644
--- a/src/main/java/org/springframework/data/mapping/context/DefaultPersistentPropertyPath.java
+++ b/src/main/java/org/springframework/data/mapping/context/DefaultPersistentPropertyPath.java
@@ -15,13 +15,18 @@
*/
package org.springframework.data.mapping.context;
+import lombok.EqualsAndHashCode;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.stream.Collectors;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -32,6 +37,7 @@ import org.springframework.util.StringUtils;
* @author Oliver Gierke
* @author Christoph Strobl
*/
+@EqualsAndHashCode
class DefaultPersistentPropertyPath> implements PersistentPropertyPath
{
private static final Converter, String> DEFAULT_CONVERTER = (source) -> source.getName();
@@ -79,7 +85,7 @@ class DefaultPersistentPropertyPath> implements
Class> leafPropertyType = getLeafProperty().getActualType();
Assert.isTrue(property.getOwner().getType().equals(leafPropertyType),
- String.format("Cannot append property %s to type %s!", property.getName(), leafPropertyType.getName()));
+ () -> String.format("Cannot append property %s to type %s!", property.getName(), leafPropertyType.getName()));
List
properties = new ArrayList<>(this.properties);
properties.add(property);
@@ -124,18 +130,12 @@ class DefaultPersistentPropertyPath
> implements
Assert.hasText(delimiter, "Delimiter must not be null or empty!");
Assert.notNull(converter, "Converter must not be null!");
- List result = new ArrayList<>();
+ String result = properties.stream() //
+ .map(converter::convert) //
+ .filter(StringUtils::hasText) //
+ .collect(Collectors.joining(delimiter));
- for (P property : properties) {
-
- String convert = converter.convert(property);
-
- if (StringUtils.hasText(convert)) {
- result.add(convert);
- }
- }
-
- return result.isEmpty() ? null : StringUtils.collectionToDelimitedString(result, delimiter);
+ return result.isEmpty() ? null : result;
}
/*
@@ -214,11 +214,7 @@ class DefaultPersistentPropertyPath> implements
int size = properties.size();
- if (size <= 1) {
- return this;
- }
-
- return new DefaultPersistentPropertyPath<>(properties.subList(0, size - 1));
+ return size == 0 ? this : new DefaultPersistentPropertyPath<>(properties.subList(0, size - 1));
}
/*
@@ -237,41 +233,18 @@ class DefaultPersistentPropertyPath
> implements
return properties.iterator();
}
- /*
- * (non-Javadoc)
- * @see org.springframework.data.mapping.context.PersistentPropertyPath#isEmpty()
+ /**
+ * Returns whether the current path contains a property of the given type.
+ *
+ * @param type can be {@literal null}.
+ * @return
*/
- public boolean isEmpty() {
- return properties.isEmpty();
- }
+ public boolean containsPropertyOfType(@Nullable TypeInformation> type) {
- /*
- * (non-Javadoc)
- * @see java.lang.Object#equals(java.lang.Object)
- */
- @Override
- public boolean equals(@Nullable Object obj) {
-
- if (this == obj) {
- return true;
- }
-
- if (obj == null || !getClass().equals(obj.getClass())) {
- return false;
- }
-
- DefaultPersistentPropertyPath> that = (DefaultPersistentPropertyPath>) obj;
-
- return this.properties.equals(that.properties);
- }
-
- /*
- * (non-Javadoc)
- * @see java.lang.Object#hashCode()
- */
- @Override
- public int hashCode() {
- return properties.hashCode();
+ return type == null //
+ ? false //
+ : properties.stream() //
+ .anyMatch(property -> type.equals(property.getTypeInformation().getActualType()));
}
/*
diff --git a/src/main/java/org/springframework/data/mapping/context/InvalidPersistentPropertyPath.java b/src/main/java/org/springframework/data/mapping/context/InvalidPersistentPropertyPath.java
index a969e517a..a89944085 100644
--- a/src/main/java/org/springframework/data/mapping/context/InvalidPersistentPropertyPath.java
+++ b/src/main/java/org/springframework/data/mapping/context/InvalidPersistentPropertyPath.java
@@ -15,10 +15,18 @@
*/
package org.springframework.data.mapping.context;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.springframework.beans.PropertyMatches;
import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
/**
* Exception to indicate a source path couldn't be resolved into a {@link PersistentPropertyPath} completely.
@@ -29,6 +37,7 @@ import org.springframework.util.Assert;
public class InvalidPersistentPropertyPath extends MappingException {
private static final long serialVersionUID = 2805815643641094488L;
+ private static final String DEFAULT_MESSAGE = "No property '%s' found on %s! Did you mean: %s?";
private final String source, unresolvableSegment, resolvedPath;
private final TypeInformation> type;
@@ -41,10 +50,11 @@ public class InvalidPersistentPropertyPath extends MappingException {
* @param resolvedPath
* @param message must not be {@literal null} or empty.
*/
- InvalidPersistentPropertyPath(String source, TypeInformation> type, String unresolvableSegment,
- @Nullable String resolvedPath, String message) {
+ public InvalidPersistentPropertyPath(String source, TypeInformation> type, String unresolvableSegment,
+ PersistentPropertyPath extends PersistentProperty>> resolvedPath) {
- super(message);
+ super(createMessage(resolvedPath.isEmpty() ? type : resolvedPath.getRequiredLeafProperty().getTypeInformation(),
+ unresolvableSegment));
Assert.notNull(source, "Source property path must not be null!");
Assert.notNull(type, "Type must not be null!");
@@ -53,7 +63,7 @@ public class InvalidPersistentPropertyPath extends MappingException {
this.source = source;
this.type = type;
this.unresolvableSegment = unresolvableSegment;
- this.resolvedPath = resolvedPath == null ? "" : resolvedPath;
+ this.resolvedPath = toDotPathOrEmpty(resolvedPath);
}
/**
@@ -91,4 +101,32 @@ public class InvalidPersistentPropertyPath extends MappingException {
public String getResolvedPath() {
return resolvedPath;
}
+
+ private static String toDotPathOrEmpty(@Nullable PersistentPropertyPath extends PersistentProperty>> path) {
+
+ if (path == null) {
+ return "";
+ }
+
+ String dotPath = path.toDotPath();
+
+ return dotPath == null ? "" : dotPath;
+ }
+
+ private static String createMessage(TypeInformation> type, String unresolvableSegment) {
+
+ Set potentialMatches = detectPotentialMatches(unresolvableSegment, type.getType());
+ Object match = StringUtils.collectionToCommaDelimitedString(potentialMatches);
+
+ return String.format(DEFAULT_MESSAGE, unresolvableSegment, type.getType(), match);
+ }
+
+ private static Set detectPotentialMatches(String propertyName, Class> type) {
+
+ Set result = new HashSet<>();
+ result.addAll(Arrays.asList(PropertyMatches.forField(propertyName, type).getPossibleMatches()));
+ result.addAll(Arrays.asList(PropertyMatches.forProperty(propertyName, type).getPossibleMatches()));
+
+ return result;
+ }
}
diff --git a/src/main/java/org/springframework/data/mapping/context/MappingContext.java b/src/main/java/org/springframework/data/mapping/context/MappingContext.java
index 8a2b560d5..6d640d935 100644
--- a/src/main/java/org/springframework/data/mapping/context/MappingContext.java
+++ b/src/main/java/org/springframework/data/mapping/context/MappingContext.java
@@ -16,10 +16,13 @@
package org.springframework.data.mapping.context;
import java.util.Collection;
+import java.util.function.Predicate;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
@@ -184,6 +187,19 @@ public interface MappingContext, P extends Pers
@Deprecated
PersistentPropertyPath getPersistentPropertyPath(InvalidPersistentPropertyPath invalidPath);
+ /**
+ * Returns all {@link PersistentPropertyPath}s pointing to properties on the given type that match the given
+ * {@link Predicate}. In case of circular references the detection will stop at the property that references a type
+ * that's already included in the path. Note, that is is a potentially expensive operation as results cannot be
+ * cached.
+ *
+ * @param type must not be {@literal null}.
+ * @param predicate must not be {@literal null}.
+ * @return
+ * @since 2.1
+ */
+ PersistentPropertyPaths findPersistentPropertyPaths(Class type, Predicate super P> predicate);
+
/**
* Returns the {@link TypeInformation}s for all {@link PersistentEntity}s in the {@link MappingContext}.
*
diff --git a/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java b/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java
index 6e8e4d80d..3a0c864bf 100644
--- a/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java
+++ b/src/main/java/org/springframework/data/mapping/context/PersistentEntities.java
@@ -17,6 +17,7 @@ package org.springframework.data.mapping.context;
import java.util.Iterator;
import java.util.Optional;
+import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.springframework.data.mapping.PersistentEntity;
@@ -33,7 +34,7 @@ import org.springframework.util.Assert;
*/
public class PersistentEntities implements Streamable>> {
- private final Streamable extends MappingContext, ?>> contexts;
+ private final Streamable extends MappingContext, ? extends PersistentProperty>>> contexts;
/**
* Creates a new {@link PersistentEntities} for the given {@link MappingContext}s.
@@ -78,6 +79,26 @@ public class PersistentEntities implements Streamable new IllegalArgumentException(String.format("Couldn't find PersistentEntity for type %s!", type)));
}
+ /**
+ * Executes the given {@link BiFunction} on the given {@link MappingContext} and {@link PersistentEntity} based on the
+ * given type.
+ *
+ * @param type must not be {@literal null}.
+ * @param combiner must not be {@literal null}.
+ * @return
+ */
+ public Optional mapOnContext(Class> type,
+ BiFunction>, PersistentEntity, ?>, T> combiner) {
+
+ Assert.notNull(type, "Type must not be null!");
+ Assert.notNull(combiner, "Combining BiFunction must not be null!");
+
+ return contexts.stream() //
+ .filter(it -> it.hasPersistentEntityFor(type)) //
+ .map(it -> combiner.apply(it, it.getRequiredPersistentEntity(type))) //
+ .findFirst();
+ }
+
/**
* Returns all {@link TypeInformation} exposed by the registered {@link MappingContext}s.
*
diff --git a/src/main/java/org/springframework/data/mapping/context/PersistentPropertyPathFactory.java b/src/main/java/org/springframework/data/mapping/context/PersistentPropertyPathFactory.java
new file mode 100644
index 000000000..a6faf1d77
--- /dev/null
+++ b/src/main/java/org/springframework/data/mapping/context/PersistentPropertyPathFactory.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mapping.context;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import lombok.Value;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import org.springframework.data.mapping.AssociationHandler;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.mapping.PersistentPropertyPaths;
+import org.springframework.data.mapping.PropertyHandler;
+import org.springframework.data.mapping.PropertyPath;
+import org.springframework.data.util.ClassTypeInformation;
+import org.springframework.data.util.Pair;
+import org.springframework.data.util.StreamUtils;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ConcurrentReferenceHashMap;
+import org.springframework.util.StringUtils;
+
+/**
+ * A factory implementation to create {@link PersistentPropertyPath} instances in various ways.
+ *
+ * @author Oliver Gierke
+ * @since 2.1
+ * @soundtrack Cypress Hill - Boom Biddy Bye Bye (Fugees Remix, Unreleased & Revamped)
+ */
+@RequiredArgsConstructor
+class PersistentPropertyPathFactory, P extends PersistentProperty> {
+
+ private static final Predicate> IS_ENTITY = it -> it.isEntity();
+
+ private final Map> propertyPaths = new ConcurrentReferenceHashMap<>();
+ private final MappingContext context;
+
+ /**
+ * 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}.
+ * @return
+ */
+ public PersistentPropertyPath from(Class> type, String propertyPath) {
+
+ Assert.notNull(type, "Type must not be null!");
+ Assert.notNull(propertyPath, "Property path must not be null!");
+
+ return getPersistentPropertyPath(ClassTypeInformation.from(type), propertyPath);
+ }
+
+ /**
+ * 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}.
+ * @return
+ */
+ public PersistentPropertyPath
from(TypeInformation> type, String propertyPath) {
+
+ Assert.notNull(type, "Type must not be null!");
+ Assert.notNull(propertyPath, "Property path must not be null!");
+
+ return getPersistentPropertyPath(type, propertyPath);
+ }
+
+ /**
+ * Creates a new {@link PersistentPropertyPath} for the given {@link PropertyPath}.
+ *
+ * @param path must not be {@literal null}.
+ * @return
+ */
+ public PersistentPropertyPath
from(PropertyPath path) {
+
+ Assert.notNull(path, "Property path must not be null!");
+
+ return from(path.getOwningType(), path.toDotPath());
+ }
+
+ /**
+ * Creates a new {@link PersistentPropertyPath} based on a given type and {@link Predicate} to select properties
+ * matching it.
+ *
+ * @param type must not be {@literal null}.
+ * @param propertyFilter must not be {@literal null}.
+ * @return
+ */
+ public PersistentPropertyPaths from(Class type, Predicate super P> propertyFilter) {
+
+ Assert.notNull(type, "Type must not be null!");
+ Assert.notNull(propertyFilter, "Property filter must not be null!");
+
+ return from(ClassTypeInformation.from(type), propertyFilter);
+ }
+
+ /**
+ * Creates a new {@link PersistentPropertyPath} based on a given type and {@link Predicate} to select properties
+ * matching it.
+ *
+ * @param type must not be {@literal null}.
+ * @param propertyFilter must not be {@literal null}.
+ * @param traversalGuard must not be {@literal null}.
+ * @return
+ */
+ public PersistentPropertyPaths from(Class type, Predicate super P> propertyFilter,
+ Predicate traversalGuard) {
+
+ Assert.notNull(type, "Type must not be null!");
+ Assert.notNull(propertyFilter, "Property filter must not be null!");
+ Assert.notNull(traversalGuard, "Traversal guard must not be null!");
+
+ return from(ClassTypeInformation.from(type), propertyFilter, traversalGuard);
+ }
+
+ /**
+ * Creates a new {@link PersistentPropertyPath} based on a given type and {@link Predicate} to select properties
+ * matching it.
+ *
+ * @param type must not be {@literal null}.
+ * @param propertyFilter must not be {@literal null}.
+ * @return
+ */
+ public PersistentPropertyPaths from(TypeInformation type, Predicate super P> propertyFilter) {
+ return from(type, propertyFilter, it -> !it.isAssociation());
+ }
+
+ /**
+ * Creates a new {@link PersistentPropertyPath} based on a given type and {@link Predicate} to select properties
+ * matching it.
+ *
+ * @param type must not be {@literal null}.
+ * @param propertyFilter must not be {@literal null}.
+ * @param traversalGuard must not be {@literal null}.
+ * @return
+ */
+ public PersistentPropertyPaths from(TypeInformation type, Predicate super P> propertyFilter,
+ Predicate traversalGuard) {
+
+ Assert.notNull(type, "Type must not be null!");
+ Assert.notNull(propertyFilter, "Property filter must not be null!");
+ Assert.notNull(traversalGuard, "Traversal guard must not be null!");
+
+ return DefaultPersistentPropertyPaths.of(type,
+ from(type, propertyFilter, traversalGuard, DefaultPersistentPropertyPath.empty()));
+ }
+
+ private PersistentPropertyPath
getPersistentPropertyPath(TypeInformation> type, String propertyPath) {
+
+ return propertyPaths.computeIfAbsent(TypeAndPath.of(type, propertyPath),
+ it -> createPersistentPropertyPath(it.getPath(), it.getType()));
+ }
+
+ /**
+ * Creates a {@link PersistentPropertyPath} for the given parts and {@link TypeInformation}.
+ *
+ * @param propertyPath must not be {@literal null}.
+ * @param type must not be {@literal null}.
+ * @return
+ */
+ private PersistentPropertyPath
createPersistentPropertyPath(String propertyPath, TypeInformation> type) {
+
+ String trimmedPath = propertyPath.trim();
+
+ List parts = trimmedPath.isEmpty() //
+ ? Collections.emptyList() //
+ : Arrays.asList(trimmedPath.split("\\."));
+
+ DefaultPersistentPropertyPath path = DefaultPersistentPropertyPath.empty();
+ Iterator iterator = parts.iterator();
+ E current = context.getRequiredPersistentEntity(type);
+
+ while (iterator.hasNext()) {
+
+ String segment = iterator.next();
+ final DefaultPersistentPropertyPath currentPath = path;
+
+ Pair, E> pair = getPair(path, iterator, segment, current);
+
+ if (pair == null) {
+
+ String source = StringUtils.collectionToDelimitedString(parts, ".");
+
+ throw new InvalidPersistentPropertyPath(source, type, segment, currentPath);
+ }
+
+ path = pair.getFirst();
+ current = pair.getSecond();
+ }
+
+ return path;
+ }
+
+ @Nullable
+ private Pair, E> getPair(DefaultPersistentPropertyPath path,
+ Iterator iterator, String segment, E entity) {
+
+ P property = entity.getPersistentProperty(segment);
+
+ if (property == null) {
+ return null;
+ }
+
+ TypeInformation> type = property.getTypeInformation().getRequiredActualType();
+ return Pair.of(path.append(property), iterator.hasNext() ? context.getRequiredPersistentEntity(type) : entity);
+ }
+
+ private Collection> from(TypeInformation type, Predicate super P> filter,
+ Predicate traversalGuard, DefaultPersistentPropertyPath
basePath) {
+
+ TypeInformation> actualType = type.getActualType();
+
+ if (actualType == null) {
+ return Collections.emptyList();
+ }
+
+ E entity = context.getRequiredPersistentEntity(actualType);
+ Set> properties = new HashSet<>();
+
+ PropertyHandler propertyTester = persistentProperty -> {
+
+ TypeInformation> typeInformation = persistentProperty.getTypeInformation();
+ TypeInformation> actualPropertyType = typeInformation.getActualType();
+
+ if (basePath.containsPropertyOfType(actualPropertyType)) {
+ return;
+ }
+
+ DefaultPersistentPropertyPath
currentPath = basePath.append(persistentProperty);
+
+ if (filter.test(persistentProperty)) {
+ properties.add(currentPath);
+ }
+
+ if (traversalGuard.and(IS_ENTITY).test(persistentProperty)) {
+ properties.addAll(from(typeInformation, filter, traversalGuard, currentPath));
+ }
+ };
+
+ entity.doWithProperties(propertyTester);
+
+ AssociationHandler
handler = association -> propertyTester.doWithPersistentProperty(association.getInverse());
+ entity.doWithAssociations(handler);
+
+ return properties;
+ }
+
+ @Value(staticConstructor = "of")
+ static class TypeAndPath {
+
+ TypeInformation> type;
+ String path;
+ }
+
+ @ToString
+ @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+ static class DefaultPersistentPropertyPaths>
+ implements PersistentPropertyPaths {
+
+ private static final Comparator> SHORTEST_PATH = Comparator
+ .comparingInt(PersistentPropertyPath::getLength);
+
+ private final TypeInformation type;
+ private final Iterable> paths;
+
+ /**
+ * Creates a new {@link DefaultPersistentPropertyPaths} instance
+ *
+ * @param type
+ * @param paths
+ * @return
+ */
+ static > PersistentPropertyPaths of(TypeInformation type,
+ Collection> paths) {
+
+ List> sorted = new ArrayList<>(paths);
+
+ Collections.sort(sorted, SHORTEST_PATH.thenComparing(ShortestSegmentFirst.INSTANCE));
+
+ return new DefaultPersistentPropertyPaths<>(type, sorted);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mapping.PersistentPropertyPaths#getFirst()
+ */
+ @Override
+ public Optional> getFirst() {
+ return isEmpty() ? Optional.empty() : Optional.of(iterator().next());
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mapping.PersistentPropertyPaths#contains(java.lang.String)
+ */
+ @Override
+ public boolean contains(String path) {
+ return contains(PropertyPath.from(path, type));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mapping.PersistentPropertyPaths#contains(org.springframework.data.mapping.PropertyPath)
+ */
+ @Override
+ public boolean contains(PropertyPath path) {
+
+ Assert.notNull(path, "PropertyPath must not be null!");
+
+ if (!path.getOwningType().equals(type)) {
+ return false;
+ }
+
+ String dotPath = path.toDotPath();
+
+ return stream().anyMatch(it -> dotPath.equals(it.toDotPath()));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Iterable#iterator()
+ */
+ @Override
+ public Iterator> iterator() {
+ return paths.iterator();
+ }
+
+ /**
+ * Simple {@link Comparator} to sort {@link PersistentPropertyPath} instances by their property segment's name
+ * length.
+ *
+ * @author Oliver Gierke
+ * @since 2.1
+ */
+ private static enum ShortestSegmentFirst
+ implements Comparator>> {
+
+ INSTANCE;
+
+ @Override
+ @SuppressWarnings("null")
+ public int compare(PersistentPropertyPath> left, PersistentPropertyPath> right) {
+
+ Function, Integer> mapper = it -> it.getName().length();
+
+ Stream leftNames = left.stream().map(mapper);
+ Stream rightNames = right.stream().map(mapper);
+
+ return StreamUtils.zip(leftNames, rightNames, (l, r) -> l - r) //
+ .filter(it -> it != 0) //
+ .findFirst() //
+ .orElse(0);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java
index f59987762..aa6610f9c 100644
--- a/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java
+++ b/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java
@@ -130,13 +130,12 @@ public abstract class AbstractPersistentProperty
return information;
}
- /*
+ /*
* (non-Javadoc)
- * @see org.springframework.data.mapping.PersistentProperty#getPersistentEntityType()
+ * @see org.springframework.data.mapping.PersistentProperty#getPersistentEntityTypes()
*/
- @Deprecated
@Override
- public Iterable extends TypeInformation>> getPersistentEntityType() {
+ public Iterable extends TypeInformation>> getPersistentEntityTypes() {
if (!isEntity()) {
return Collections.emptySet();
diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java
index 8b235f1f9..a357cfe6f 100644
--- a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java
+++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java
@@ -217,10 +217,9 @@ public class RepositoryComposition {
*/
public void validateImplementation() {
- fragments.stream()
- .forEach(it -> it.getImplementation() //
- .orElseThrow(() -> new IllegalStateException(String.format("Fragment %s has no implementation.",
- ClassUtils.getQualifiedName(it.getSignatureContributor())))));
+ fragments.stream().forEach(it -> it.getImplementation() //
+ .orElseThrow(() -> new IllegalStateException(String.format("Fragment %s has no implementation.",
+ ClassUtils.getQualifiedName(it.getSignatureContributor())))));
}
/**
@@ -320,15 +319,6 @@ public class RepositoryComposition {
return from(Stream.concat(left, right).collect(Collectors.toList()));
}
- /**
- * Return {@literal true} if this {@link RepositoryFragments} contains no {@link RepositoryFragment fragments}.
- *
- * @return {@literal true} if this {@link RepositoryFragments} contains no {@link RepositoryFragment fragments}.
- */
- public boolean isEmpty() {
- return fragments.isEmpty();
- }
-
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
@@ -338,14 +328,6 @@ public class RepositoryComposition {
return fragments.iterator();
}
- /**
- * @return {@link Stream} of {@link RepositoryFragment fragments}.
- */
- @Override
- public Stream> stream() {
- return fragments.stream();
- }
-
/**
* @return {@link Stream} of {@link Method methods}.
*/
diff --git a/src/main/java/org/springframework/data/util/StreamUtils.java b/src/main/java/org/springframework/data/util/StreamUtils.java
index 54315ba45..c3070299d 100644
--- a/src/main/java/org/springframework/data/util/StreamUtils.java
+++ b/src/main/java/org/springframework/data/util/StreamUtils.java
@@ -23,6 +23,9 @@ import java.util.List;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
+import java.util.Spliterators.AbstractSpliterator;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Stream;
@@ -107,4 +110,39 @@ public interface StreamUtils {
public static Stream fromNullable(@Nullable T source) {
return source == null ? Stream.empty() : Stream.of(source);
}
+
+ /**
+ * Zips the given {@link Stream}s using the given {@link BiFunction}. The resulting {@link Stream} will have the
+ * length of the shorter of the two, abbreviating the zipping when the shorter of the two {@link Stream}s is
+ * exhausted.
+ *
+ * @param left must not be {@literal null}.
+ * @param right must not be {@literal null}.
+ * @param combiner must not be {@literal null}.
+ * @return
+ * @since 2.1
+ */
+ public static Stream zip(Stream left, Stream right, BiFunction combiner) {
+
+ Assert.notNull(left, "Left stream must not be null!");
+ Assert.notNull(right, "Right must not be null!");
+ Assert.notNull(combiner, "Combiner must not be null!");
+
+ Spliterator lefts = left.spliterator();
+ Spliterator rights = right.spliterator();
+
+ long size = Long.min(lefts.estimateSize(), rights.estimateSize());
+ int characteristics = lefts.characteristics() & rights.characteristics();
+ boolean parallel = left.isParallel() || right.isParallel();
+
+ return StreamSupport.stream(new AbstractSpliterator(size, characteristics) {
+
+ @Override
+ @SuppressWarnings("null")
+ public boolean tryAdvance(Consumer super T> action) {
+ return lefts.tryAdvance(left -> rights.tryAdvance(right -> action.accept(combiner.apply(left, right))));
+ }
+
+ }, parallel);
+ }
}
diff --git a/src/main/java/org/springframework/data/util/Streamable.java b/src/main/java/org/springframework/data/util/Streamable.java
index 68aed2ac7..a3a0802e4 100644
--- a/src/main/java/org/springframework/data/util/Streamable.java
+++ b/src/main/java/org/springframework/data/util/Streamable.java
@@ -121,4 +121,13 @@ public interface Streamable extends Iterable {
return Streamable.of(() -> stream().filter(predicate));
}
+
+ /**
+ * Returns whether the current {@link Streamable} is empty.
+ *
+ * @return
+ */
+ default boolean isEmpty() {
+ return !iterator().hasNext();
+ }
}
diff --git a/src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java b/src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java
new file mode 100644
index 000000000..271f47cd1
--- /dev/null
+++ b/src/test/java/org/springframework/data/mapping/PersistentPropertyAccessorUnitTests.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mapping;
+
+import static org.assertj.core.api.Assertions.*;
+
+import lombok.Value;
+
+import org.junit.Test;
+import org.springframework.data.mapping.context.SampleMappingContext;
+
+/**
+ * @author Oliver Gierke
+ */
+public class PersistentPropertyAccessorUnitTests {
+
+ SampleMappingContext context = new SampleMappingContext();
+
+ PersistentPropertyPath extends PersistentProperty>> path;
+ PersistentPropertyAccessor accessor;
+
+ private void setUp(Order order, String path) {
+
+ this.accessor = context.getPersistentEntity(Order.class).getPropertyAccessor(order);
+ this.path = context.getPersistentPropertyPath(path, Order.class);
+ }
+
+ @Test // DATACMNS-1275
+ public void looksUpValueForPropertyPath() {
+
+ Order order = new Order(new Customer("Dave"));
+
+ setUp(order, "customer.firstname");
+
+ assertThat(accessor.getProperty(path)).isEqualTo("Dave");
+ }
+
+ @Test // DATACMNS-1275
+ public void setsPropertyOnNestedPath() {
+
+ Customer customer = new Customer("Dave");
+ Order order = new Order(customer);
+
+ setUp(order, "customer.firstname");
+
+ accessor.setProperty(path, "Oliver August");
+
+ assertThat(customer.firstname).isEqualTo("Oliver August");
+ }
+
+ @Test // DATACMNS-1275
+ public void rejectsEmptyPathToSetValues() {
+
+ setUp(new Order(null), "");
+
+ assertThatExceptionOfType(IllegalArgumentException.class) //
+ .isThrownBy(() -> accessor.setProperty(path, "Oliver August"));
+ }
+
+ @Test // DATACMNS-1275
+ public void rejectsIntermediateNullValuesForRead() {
+
+ setUp(new Order(null), "customer.firstname");
+
+ assertThatExceptionOfType(MappingException.class)//
+ .isThrownBy(() -> accessor.getProperty(path));
+ }
+
+ @Test // DATACMNS-1275
+ public void rejectsIntermediateNullValuesForWrite() {
+
+ setUp(new Order(null), "customer.firstname");
+
+ assertThatExceptionOfType(MappingException.class)//
+ .isThrownBy(() -> accessor.setProperty(path, "Oliver August"));
+ }
+
+ @Value
+ static class Order {
+ Customer customer;
+ }
+
+ @Value
+ static class Customer {
+ String firstname;
+ }
+}
diff --git a/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java b/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java
index 23f63b7b3..0ab05b9b3 100755
--- a/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java
@@ -34,12 +34,10 @@ import org.springframework.context.ApplicationEvent;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
-import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
-import org.springframework.util.StringUtils;
/**
* Unit test for {@link AbstractMappingContext}.
@@ -54,17 +52,11 @@ public class AbstractMappingContextUnitTests {
@Before
public void setUp() {
+
context = new SampleMappingContext();
context.setSimpleTypeHolder(new SimpleTypeHolder(Collections.singleton(LocalDateTime.class), true));
}
- @Test
- public void doesNotTryToLookupPersistentEntityForLeafProperty() {
- PersistentPropertyPath path = context
- .getPersistentPropertyPath(PropertyPath.from("name", Person.class));
- assertThat(path).isNotNull();
- }
-
@Test(expected = MappingException.class) // DATACMNS-92
public void doesNotAddInvalidEntity() {
@@ -158,22 +150,6 @@ public class AbstractMappingContextUnitTests {
.satisfies(inner -> assertThat(((PersistentEntity) inner).getType()).isEqualTo(Person.class)));
}
- @Test // DATACMNS-380
- public void returnsPersistentPropertyPathForDotPath() {
-
- PersistentPropertyPath path = context.getPersistentPropertyPath("persons.name",
- Sample.class);
-
- assertThat(path.getLength()).isEqualTo(2);
- assertThat(path.getBaseProperty().getName()).isEqualTo("persons");
- assertThat(path.getLeafProperty().getName()).isEqualTo("name");
- }
-
- @Test(expected = MappingException.class) // DATACMNS-380
- public void rejectsInvalidPropertyReferenceWithMappingException() {
- context.getPersistentPropertyPath("foo", Sample.class);
- }
-
@Test // DATACMNS-390
public void exposesCopyOfPersistentEntitiesToAvoidConcurrentModificationException() {
@@ -220,29 +196,6 @@ public class AbstractMappingContextUnitTests {
assertThat(context.getPersistentEntity(TypeCreatingSyntheticClass.class)).isNotNull();
}
- @Test // DATACMNS-695
- public void persistentPropertyPathTraversesGenericTypesCorrectly() {
- assertThat(context.getPersistentPropertyPath("field.wrapped.field", Outer.class)).hasSize(3);
- }
-
- @Test // DATACMNS-727
- public void exposesContextForFailingPropertyPathLookup() {
-
- assertThatExceptionOfType(InvalidPersistentPropertyPath.class)//
- .isThrownBy(() -> context.getPersistentPropertyPath("persons.firstname", Sample.class))//
- .matches(e -> StringUtils.hasText(e.getMessage()))//
- .matches(e -> e.getResolvedPath().equals("persons"))//
- .matches(e -> e.getUnresolvableSegment().equals("firstname"))//
- .matches(e -> context.getPersistentPropertyPath(e) != null);
- }
-
- @Test // DATACMNS-1116
- public void cachesPersistentPropertyPaths() {
-
- assertThat(context.getPersistentPropertyPath("persons.name", Sample.class)) //
- .isSameAs(context.getPersistentPropertyPath("persons.name", Sample.class));
- }
-
@Test // DATACMNS-1208
public void ensureHasPersistentEntityReportsFalseForTypesThatShouldntBeCreated() {
@@ -299,17 +252,4 @@ public class AbstractMappingContextUnitTests {
static class Extension extends Base {
@Id String foo;
}
-
- static class Outer {
-
- Wrapper field;
- }
-
- static class Wrapper {
- T wrapped;
- }
-
- static class Inner {
- String field;
- }
}
diff --git a/src/test/java/org/springframework/data/mapping/context/DefaultPersistenPropertyPathUnitTests.java b/src/test/java/org/springframework/data/mapping/context/DefaultPersistentPropertyPathUnitTests.java
similarity index 80%
rename from src/test/java/org/springframework/data/mapping/context/DefaultPersistenPropertyPathUnitTests.java
rename to src/test/java/org/springframework/data/mapping/context/DefaultPersistentPropertyPathUnitTests.java
index 86fab1b5a..5880fd239 100755
--- a/src/test/java/org/springframework/data/mapping/context/DefaultPersistenPropertyPathUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/context/DefaultPersistentPropertyPathUnitTests.java
@@ -16,6 +16,7 @@
package org.springframework.data.mapping.context;
import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
@@ -28,6 +29,7 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.PersistentPropertyPath;
/**
* Unit tests for {@link DefaultPersistentPropertyPath}.
@@ -35,14 +37,14 @@ import org.springframework.data.mapping.PersistentProperty;
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
-public class DefaultPersistenPropertyPathUnitTests> {
+public class DefaultPersistentPropertyPathUnitTests> {
- @Mock T first, second;
+ @Mock P first, second;
- @Mock Converter converter;
+ @Mock Converter converter;
- PersistentPropertyPath oneLeg;
- PersistentPropertyPath twoLegs;
+ PersistentPropertyPath oneLeg;
+ PersistentPropertyPath
twoLegs;
@Before
public void setUp() {
@@ -96,7 +98,7 @@ public class DefaultPersistenPropertyPathUnitTests extension = twoLegs.getExtensionForBaseOf(oneLeg);
+ PersistentPropertyPath extension = twoLegs.getExtensionForBaseOf(oneLeg);
assertThat(extension).isEqualTo(new DefaultPersistentPropertyPath<>(Collections.singletonList(second)));
}
@@ -107,8 +109,18 @@ public class DefaultPersistenPropertyPathUnitTests parent = oneLeg.getParentPath();
+ PersistentPropertyPath parentsParent = parent.getParentPath();
+
+ assertThat(parentsParent).isEmpty();
+ assertThat(parentsParent).isSameAs(parent);
}
@Test
diff --git a/src/test/java/org/springframework/data/mapping/context/PersistentPropertyPathFactoryUnitTests.java b/src/test/java/org/springframework/data/mapping/context/PersistentPropertyPathFactoryUnitTests.java
new file mode 100644
index 000000000..693d81706
--- /dev/null
+++ b/src/test/java/org/springframework/data/mapping/context/PersistentPropertyPathFactoryUnitTests.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mapping.context;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.junit.Test;
+import org.springframework.data.annotation.Reference;
+import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.mapping.PersistentPropertyPaths;
+import org.springframework.data.mapping.PropertyPath;
+import org.springframework.data.mapping.model.BasicPersistentEntity;
+import org.springframework.data.util.Streamable;
+import org.springframework.util.StringUtils;
+
+/**
+ * Unit tests for {@link PersistentPropertyPathFactory}.
+ *
+ * @author Oliver Gierke
+ * @soundtrack Cypress Hill - Illusions (Q-Tip Remix, Unreleased & Revamped)
+ */
+public class PersistentPropertyPathFactoryUnitTests {
+
+ PersistentPropertyPathFactory, SamplePersistentProperty> factory = //
+ new PersistentPropertyPathFactory<>(new SampleMappingContext());
+
+ @Test
+ public void doesNotTryToLookupPersistentEntityForLeafProperty() {
+ assertThat(factory.from(Person.class, "name")).isNotNull();
+ }
+
+ @Test // DATACMNS-380
+ public void returnsPersistentPropertyPathForDotPath() {
+
+ PersistentPropertyPath path = factory.from(PersonSample.class, "persons.name");
+
+ assertThat(path.getLength()).isEqualTo(2);
+ assertThat(path.getBaseProperty().getName()).isEqualTo("persons");
+ assertThat(path.getLeafProperty().getName()).isEqualTo("name");
+ }
+
+ @Test(expected = MappingException.class) // DATACMNS-380
+ public void rejectsInvalidPropertyReferenceWithMappingException() {
+ factory.from(PersonSample.class, "foo");
+ }
+
+ @Test // DATACMNS-695
+ public void persistentPropertyPathTraversesGenericTypesCorrectly() {
+ assertThat(factory.from(Outer.class, "field.wrapped.field")).hasSize(3);
+ }
+
+ @Test // DATACMNS-727
+ public void exposesContextForFailingPropertyPathLookup() {
+
+ assertThatExceptionOfType(InvalidPersistentPropertyPath.class)//
+ .isThrownBy(() -> factory.from(PersonSample.class, "persons.firstname"))//
+ .matches(e -> StringUtils.hasText(e.getMessage()))//
+ .matches(e -> e.getResolvedPath().equals("persons"))//
+ .matches(e -> e.getUnresolvableSegment().equals("firstname"))//
+ .matches(e -> factory.from(PersonSample.class, e.getResolvedPath()) != null);
+ }
+
+ @Test // DATACMNS-1116
+ public void cachesPersistentPropertyPaths() {
+
+ assertThat(factory.from(PersonSample.class, "persons.name")) //
+ .isSameAs(factory.from(PersonSample.class, "persons.name"));
+ }
+
+ @Test // DATACMNS-1275
+ public void findsNestedPropertyByFilter() {
+
+ PersistentPropertyPaths, SamplePersistentProperty> paths = factory.from(Sample.class,
+ property -> property.findAnnotation(Inject.class) != null);
+
+ assertThat(paths).hasSize(1).anySatisfy(it -> it.toDotPath().equals("inner.annotatedField"));
+ }
+
+ @Test // DATACMNS-1275
+ public void findsNestedPropertiesByFilter() {
+
+ PersistentPropertyPaths, SamplePersistentProperty> paths = factory.from(Wrapper.class,
+ property -> property.findAnnotation(Inject.class) != null);
+
+ assertThat(paths).hasSize(2);//
+ assertThat(paths).element(0).satisfies(it -> it.toDotPath().equals("first.inner.annotatedField"));
+ assertThat(paths).element(1).satisfies(it -> it.toDotPath().equals("second.inner.annotatedField"));
+ }
+
+ @Test // DATACMNS-1275
+ public void createsEmptyPropertyPathCorrectly() {
+ assertThat(factory.from(Wrapper.class, "")).isEmpty();
+ }
+
+ @Test // DATACMNS-1275
+ public void returnsShortestsPathsFirst() {
+
+ Streamable paths = factory.from(First.class, it -> true, it -> true) //
+ .map(PersistentPropertyPath::toDotPath);
+
+ assertThat(paths).containsExactly("third", "second", "third.lastname", "second.firstname");
+ }
+
+ @Test // DATACMNS-1275
+ public void doesNotTraverseAssociationsByDefault() {
+
+ Streamable paths = factory.from(First.class, it -> true) //
+ .map(PersistentPropertyPath::toDotPath);
+
+ assertThat(paths) //
+ .contains("third", "second", "second.firstname")//
+ .doesNotContain("third.lastname");
+ }
+
+ @Test // DATACMNS-1275
+ public void traversesAssociationsIfTraversalGuardAllowsIt() {
+
+ PersistentPropertyPaths paths = //
+ factory.from(First.class, it -> true, it -> true);
+
+ assertThat(paths.contains("third.lastname")).isTrue();
+ assertThat(paths.contains(PropertyPath.from("third.lastname", First.class)));
+ }
+
+ @Test // DATACMNS-1275
+ public void returnsEmptyPropertyPathsIfNoneSelected() {
+
+ PersistentPropertyPaths paths = factory.from(Third.class, it -> false);
+
+ assertThat(paths).isEmpty();
+ assertThat(paths.getFirst()).isEmpty();
+ }
+
+ @Test // DATACMNS-1275
+ public void returnsShortestPathFirst() {
+
+ PersistentPropertyPaths paths = factory.from(First.class, it -> !it.isEntity(),
+ it -> true);
+
+ assertThat(paths.contains("second.firstname")).isTrue();
+ assertThat(paths.getFirst()) //
+ .hasValueSatisfying(it -> assertThat(it.toDotPath()).isEqualTo("third.lastname"));
+ }
+
+ static class PersonSample {
+ List persons;
+ }
+
+ class Person {
+ String name;
+ }
+
+ static class Wrapper {
+ Sample first;
+ Sample second;
+ }
+
+ static class Sample {
+ Inner inner;
+ }
+
+ static class Inner {
+ @Inject String annotatedField;
+ Sample cyclic;
+ }
+
+ static class Outer {
+ Generic field;
+ }
+
+ static class Generic {
+ T wrapped;
+ }
+
+ static class Nested {
+ String field;
+ }
+
+ static class First {
+ Second second;
+ @Reference Third third;
+ }
+
+ static class Second {
+ String firstname;
+ }
+
+ static class Third {
+ String lastname;
+ }
+}
diff --git a/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java
index ad9e1909b..def90c749 100755
--- a/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java
+++ b/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java
@@ -72,7 +72,6 @@ public class AbstractPersistentPropertyUnitTests {
@Test // DATACMNS-101
public void returnsNestedEntityTypeCorrectly() {
assertThat(getProperty(TestClassComplex.class, "testClassSet").getPersistentEntityTypes()).isEmpty();
- assertThat(getProperty(TestClassComplex.class, "testClassSet").getPersistentEntityType()).isEmpty();
}
@Test // DATACMNS-132
@@ -198,7 +197,6 @@ public class AbstractPersistentPropertyUnitTests {
SamplePersistentProperty property = getProperty(TreeMapWrapper.class, "map");
assertThat(property.getPersistentEntityTypes()).isEmpty();
- assertThat(property.getPersistentEntityType()).isEmpty();
assertThat(property.isEntity()).isFalse();
}