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> 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> getPersistentEntityTypes() { - return getPersistentEntityType(); - }; + Iterable> 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 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> path, @Nullable Object value) { + + Assert.notNull(path, "PersistentPropertyPath must not be null!"); + Assert.isTrue(!path.isEmpty(), "PersistentPropertyPath must not be empty!"); + + PersistentPropertyPath> 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> 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> 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 converter); + String toDotPath(Converter 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 converter); + String toPath(String delimiter, Converter 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 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 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> 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> 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 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> contexts; + private final Streamable>> 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 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 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 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 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 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> getPersistentEntityType() { + public Iterable> 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 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> 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 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 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(); }