From caa8c6afc5b5a86bbbc540fdb6d67ef6e453a07c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 17 Nov 2021 14:59:41 +0100 Subject: [PATCH] Refactor PropertyFilterSupport into EntityProjectionIntrospector. Original Pull Request: #2420 --- .../context/EntityProjectionDiscoverer.java | 354 -------------- .../context/EntityProjectionIntrospector.java | 445 ++++++++++++++++++ ...ntityProjectionIntrospectorUnitTests.java} | 107 +++-- 3 files changed, 510 insertions(+), 396 deletions(-) delete mode 100644 src/main/java/org/springframework/data/mapping/context/EntityProjectionDiscoverer.java create mode 100644 src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java rename src/test/java/org/springframework/data/mapping/context/{EntityProjectionDiscovererUnitTests.java => EntityProjectionIntrospectorUnitTests.java} (53%) diff --git a/src/main/java/org/springframework/data/mapping/context/EntityProjectionDiscoverer.java b/src/main/java/org/springframework/data/mapping/context/EntityProjectionDiscoverer.java deleted file mode 100644 index d99565a7c..000000000 --- a/src/main/java/org/springframework/data/mapping/context/EntityProjectionDiscoverer.java +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright 2021 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 - * - * https://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 java.beans.PropertyDescriptor; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; - -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.projection.ProjectionInformation; -import org.springframework.data.util.Pair; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * This class is introspects the returned type in the context of a domain type for all reachable properties (w/o cycles) - * to determine which property paths are subject to projection. - * - * @author Gerrit Meier - * @author Mark Paluch - * @since 2.7 - */ -public class EntityProjectionDiscoverer { - - private final ProjectionFactory projectionFactory; - private final ProjectionPredicate projectionPredicate; - private final MappingContext mappingContext; - - private EntityProjectionDiscoverer(ProjectionFactory projectionFactory, ProjectionPredicate projectionPredicate, - MappingContext mappingContext) { - this.projectionFactory = projectionFactory; - this.projectionPredicate = projectionPredicate; - this.mappingContext = mappingContext; - } - - /** - * Create a new {@link EntityProjectionDiscoverer} given {@link ProjectionFactory}, {@link ProjectionPredicate} and - * {@link MappingContext}. - * - * @param projectionFactory must not be {@literal null}. - * @param projectionPredicate must not be {@literal null}. - * @param mappingContext must not be {@literal null}. - * @return a new {@link EntityProjectionDiscoverer} instance. - */ - public static EntityProjectionDiscoverer create(ProjectionFactory projectionFactory, - ProjectionPredicate projectionPredicate, MappingContext mappingContext) { - - Assert.notNull(projectionFactory, "ProjectionFactory must not be null"); - Assert.notNull(projectionPredicate, "ProjectionPredicate must not be null"); - Assert.notNull(mappingContext, "MappingContext must not be null"); - - return new EntityProjectionDiscoverer(projectionFactory, projectionPredicate, mappingContext); - } - - /** - * Introspect a {@link Class return type} in the context of a {@link Class domain type} whether the returned type is a - * projection and what property paths are participating in the projection. - *

- * Nested properties (direct types, within maps, collections) are introspected for nested projections and contain - * property paths for closed projections. - * - * @param returnType - * @param domainType - * @return - */ - public ReturnedTypeDescriptor introspectReturnType(Class returnType, Class domainType) { - - boolean isProjection = projectionPredicate.test(returnType, domainType); - - if (!isProjection) { - return ReturnedTypeDescriptor.nonProjecting(returnType, domainType, Collections.emptyList()); - } - - ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType); - - if (!projectionInformation.isClosed()) { - return ReturnedTypeDescriptor.projecting(returnType, domainType, Collections.emptyList()); - } - - Set, Class>> cycleGuard = new HashSet<>(); - - PersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(domainType); - List propertyDescriptors = getProperties(null, projectionInformation, - persistentEntity, cycleGuard); - - return ReturnedTypeDescriptor.projecting(returnType, domainType, propertyDescriptors); - } - - private List getProperties(@Nullable PropertyPath propertyPath, - ProjectionInformation projectionInformation, PersistentEntity persistentEntity, - Set, Class>> cycleGuard) { - - List propertyDescriptors = new ArrayList<>(); - for (PropertyDescriptor inputProperty : projectionInformation.getInputProperties()) { - - PersistentProperty persistentProperty = persistentEntity.getPersistentProperty(inputProperty.getName()); - - if (persistentProperty == null) { - continue; - } - - Class returnedType = inputProperty.getPropertyType(); - Class domainType = persistentProperty.getActualType(); - - PropertyPath nestedPropertyPath = propertyPath == null - ? PropertyPath.from(persistentProperty.getName(), persistentEntity.getTypeInformation()) - : propertyPath.nested(persistentProperty.getName()); - - if (projectionPredicate.test(returnedType, domainType)) { - - List nestedPropertyDescriptors; - - if (cycleGuard.add(Pair.of(returnedType, domainType))) { - nestedPropertyDescriptors = getProjectedProperties(nestedPropertyPath, returnedType, domainType, cycleGuard); - } else { - nestedPropertyDescriptors = Collections.emptyList(); - } - - propertyDescriptors.add(PropertyProjectionDescriptor.projecting(nestedPropertyPath, returnedType, domainType, - nestedPropertyDescriptors)); - } else { - propertyDescriptors - .add(PropertyProjectionDescriptor.nonProjecting(nestedPropertyPath, returnedType, domainType)); - } - } - - return propertyDescriptors; - } - - private List getProjectedProperties(PropertyPath propertyPath, Class returnedType, - Class domainType, Set, Class>> cycleGuard) { - - ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnedType); - PersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(domainType); - - // Closed projection should get handled as above (recursion) - return projectionInformation.isClosed() - ? getProperties(propertyPath, projectionInformation, persistentEntity, cycleGuard) - : Collections.emptyList(); - } - - /** - * Descriptor for a top-level return type. - */ - public static class ReturnedTypeDescriptor { - - private final Class returnedType; - private final Class domainType; - private final List nested; - private final boolean projecting; - - ReturnedTypeDescriptor(Class returnedType, Class domainType, List nested, - boolean projecting) { - this.domainType = domainType; - this.returnedType = returnedType; - this.nested = nested; - this.projecting = projecting; - } - - /** - * Create a projecting variant of a return type. - * - * @param returnedType - * @param domainType - * @param nested - * @return - */ - public static ReturnedTypeDescriptor projecting(Class returnedType, Class domainType, - List nested) { - return new ReturnedTypeDescriptor(returnedType, domainType, nested, true); - } - - /** - * Create a non-projecting variant of a return type. - * - * @param returnedType - * @param domainType - * @param nested - * @return - */ - public static ReturnedTypeDescriptor nonProjecting(Class returnedType, Class domainType, - List nested) { - return new ReturnedTypeDescriptor(returnedType, domainType, nested, false); - } - - public Class getDomainType() { - return domainType; - } - - public Class getReturnedType() { - return returnedType; - } - - public boolean isProjecting() { - return projecting; - } - - List getNested() { - return nested; - } - - /** - * Perform the given {@code action} for each element of the {@code ReturnedTypeDescriptor} until all elements have - * been processed or the action throws an exception. - * - * @param action the action to be performed for each element - */ - public void forEach(Consumer action) { - - for (PropertyProjectionDescriptor descriptor : nested) { - - if (descriptor.getNested().isEmpty()) { - action.accept(descriptor.getPropertyPath()); - } else { - descriptor.forEach(action); - } - } - } - - @Override - public String toString() { - - if (isProjecting()) { - return String.format("Projection(%s AS %s): %s", getDomainType().getName(), getReturnedType().getName(), - nested); - } - - return String.format("Domain(%s): %s", getReturnedType().getName(), nested); - } - } - - /** - * Descriptor for a property-level type along its potential projection. - */ - public static class PropertyProjectionDescriptor extends ReturnedTypeDescriptor { - - private final PropertyPath propertyPath; - - PropertyProjectionDescriptor(PropertyPath propertyPath, Class returnedType, Class domainType, - List nested, boolean projecting) { - super(returnedType, domainType, nested, projecting); - this.propertyPath = propertyPath; - } - - /** - * Create a projecting variant of a return type. - * - * @param propertyPath - * @param returnedType - * @param domainType - * @param nested - * @return - */ - public static PropertyProjectionDescriptor projecting(PropertyPath propertyPath, Class returnedType, - Class domainType, List nested) { - return new PropertyProjectionDescriptor(propertyPath, returnedType, domainType, nested, true); - } - - /** - * Create a non-projecting variant of a return type. - * - * @param propertyPath - * @param returnedType - * @param domainType - * @return - */ - public static PropertyProjectionDescriptor nonProjecting(PropertyPath propertyPath, Class returnedType, - Class domainType) { - return new PropertyProjectionDescriptor(propertyPath, returnedType, domainType, Collections.emptyList(), false); - } - - public PropertyPath getPropertyPath() { - return propertyPath; - } - - @Override - public String toString() { - return String.format("%s AS %s", propertyPath.toDotPath(), getReturnedType().getName()); - } - } - - /** - * Represents a predicate (boolean-valued function) of a {@link Class target type} and its {@link Class underlying - * type}. - */ - public interface ProjectionPredicate { - - /** - * Evaluates this predicate on the given arguments. - * - * @param target the target type. - * @param target the underlying type. - * @return {@code true} if the input argument matches the predicate, otherwise {@code false}. - */ - boolean test(Class target, Class underlyingType); - - /** - * Return a composed predicate that represents a short-circuiting logical AND of this predicate and another. When - * evaluating the composed predicate, if this predicate is {@code false}, then the {@code other} predicate is not - * evaluated. - *

- * Any exceptions thrown during evaluation of either predicate are relayed to the caller; if evaluation of this - * predicate throws an exception, the {@code other} predicate will not be evaluated. - * - * @param other a predicate that will be logically-ANDed with this predicate - * @return a composed predicate that represents the short-circuiting logical AND of this predicate and the - * {@code other} predicate - */ - default ProjectionPredicate and(ProjectionPredicate other) { - return (target, underlyingType) -> test(target, underlyingType) && other.test(target, underlyingType); - } - - /** - * Return a predicate that represents the logical negation of this predicate. - * - * @return a predicate that represents the logical negation of this predicate - */ - default ProjectionPredicate negate() { - return (target, underlyingType) -> !test(target, underlyingType); - } - - /** - * Return a predicate that considers whether the {@code target type} is participating in the type hierarchy. - */ - static ProjectionPredicate typeHierarchy() { - - ProjectionPredicate predicate = (target, underlyingType) -> target.isAssignableFrom(underlyingType) || // hierarchy - underlyingType.isAssignableFrom(target); - return predicate.negate(); - } - - } - -} diff --git a/src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java b/src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java new file mode 100644 index 000000000..e3d759718 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java @@ -0,0 +1,445 @@ +/* + * Copyright 2021 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 + * + * https://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 java.beans.PropertyDescriptor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.ProjectionInformation; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * This class is introspects the returned type in the context of a domain type for all reachable properties (w/o cycles) + * to determine which property paths are subject to projection. + * + * @author Gerrit Meier + * @author Mark Paluch + * @since 2.7 + */ +public class EntityProjectionIntrospector { + + private final ProjectionFactory projectionFactory; + private final ProjectionPredicate projectionPredicate; + private final MappingContext mappingContext; + + private EntityProjectionIntrospector(ProjectionFactory projectionFactory, ProjectionPredicate projectionPredicate, + MappingContext mappingContext) { + this.projectionFactory = projectionFactory; + this.projectionPredicate = projectionPredicate; + this.mappingContext = mappingContext; + } + + /** + * Create a new {@link EntityProjectionIntrospector} given {@link ProjectionFactory}, {@link ProjectionPredicate} and + * {@link MappingContext}. + * + * @param projectionFactory must not be {@literal null}. + * @param projectionPredicate must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + * @return a new {@link EntityProjectionIntrospector} instance. + */ + public static EntityProjectionIntrospector create(ProjectionFactory projectionFactory, + ProjectionPredicate projectionPredicate, MappingContext mappingContext) { + + Assert.notNull(projectionFactory, "ProjectionFactory must not be null"); + Assert.notNull(projectionPredicate, "ProjectionPredicate must not be null"); + Assert.notNull(mappingContext, "MappingContext must not be null"); + + return new EntityProjectionIntrospector(projectionFactory, projectionPredicate, mappingContext); + } + + /** + * Introspect a {@link Class mapped type} in the context of a {@link Class domain type} whether the returned type is a + * projection and what property paths are participating in the projection. + *

+ * Nested properties (direct types, within maps, collections) are introspected for nested projections and contain + * property paths for closed projections. + * + * @param mappedType + * @param domainType + * @return + */ + public EntityProjection introspect(Class mappedType, Class domainType) { + + ClassTypeInformation returnedTypeInformation = ClassTypeInformation.from(mappedType); + ClassTypeInformation domainTypeInformation = ClassTypeInformation.from(domainType); + + boolean isProjection = projectionPredicate.test(mappedType, domainType); + + if (!isProjection) { + return EntityProjection.nonProjecting(returnedTypeInformation, domainTypeInformation, Collections.emptyList()); + } + + ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(mappedType); + + if (!projectionInformation.isClosed()) { + return EntityProjection.projecting(returnedTypeInformation, domainTypeInformation, Collections.emptyList(), + false); + } + + + PersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(domainType); + List> propertyDescriptors = getProperties(null, projectionInformation, + returnedTypeInformation, + persistentEntity, null); + + return EntityProjection.projecting(returnedTypeInformation, domainTypeInformation, propertyDescriptors, true); + } + + + private List> getProperties(@Nullable PropertyPath propertyPath, + ProjectionInformation projectionInformation, TypeInformation projectionTypeInformation, + PersistentEntity persistentEntity, @Nullable CycleGuard cycleGuard) { + + List> propertyDescriptors = new ArrayList<>(); + for (PropertyDescriptor inputProperty : projectionInformation.getInputProperties()) { + + PersistentProperty persistentProperty = persistentEntity.getPersistentProperty(inputProperty.getName()); + + if (persistentProperty == null) { + continue; + } + + CycleGuard cycleGuardToUse = cycleGuard != null ? cycleGuard : new CycleGuard(); + + TypeInformation property = projectionTypeInformation.getRequiredProperty(inputProperty.getName()); + + PropertyPath nestedPropertyPath = propertyPath == null + ? PropertyPath.from(persistentProperty.getName(), persistentEntity.getTypeInformation()) + : propertyPath.nested(persistentProperty.getName()); + + TypeInformation returnedType = property.getRequiredActualType(); + TypeInformation domainType = persistentProperty.getTypeInformation().getRequiredActualType(); + + if (isProjection(returnedType, domainType)) { + + List> nestedPropertyDescriptors; + + if (cycleGuardToUse.isCycleFree(persistentProperty)) { + nestedPropertyDescriptors = getProjectedProperties(nestedPropertyPath, returnedType, domainType, + cycleGuardToUse); + } else { + nestedPropertyDescriptors = Collections.emptyList(); + } + + propertyDescriptors.add(PropertyProjection.projecting(nestedPropertyPath, property, + persistentProperty.getTypeInformation(), + nestedPropertyDescriptors, projectionInformation.isClosed())); + } else { + propertyDescriptors + .add(PropertyProjection.nonProjecting(nestedPropertyPath, property, + persistentProperty.getTypeInformation())); + } + } + + return propertyDescriptors; + } + + private boolean isProjection(TypeInformation returnedType, TypeInformation domainType) { + return projectionPredicate.test(returnedType.getRequiredActualType().getType(), + domainType.getRequiredActualType().getType()); + } + + private List> getProjectedProperties(PropertyPath propertyPath, + TypeInformation returnedType, TypeInformation domainType, CycleGuard cycleGuard) { + + ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnedType.getType()); + PersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(domainType); + + // Closed projection should get handled as above (recursion) + return projectionInformation.isClosed() + ? getProperties(propertyPath, projectionInformation, returnedType, persistentEntity, cycleGuard) + : Collections.emptyList(); + } + + /** + * Descriptor for a top-level mapped type representing a view onto a domain type structure. The view may exactly match + * the domain type or be a DTO/interface {@link #isProjection() projection}. + * + * @param the mapped type acting as view onto the domain type. + * @param the domain type. + */ + public static class EntityProjection { + + private final TypeInformation mappedType; + private final TypeInformation domainType; + private final List> properties; + private final boolean projection; + private final boolean closedProjection; + + EntityProjection(TypeInformation mappedType, TypeInformation domainType, + List> properties, boolean projection, boolean closedProjection) { + this.mappedType = mappedType; + this.domainType = domainType; + this.properties = properties; + this.projection = projection; + this.closedProjection = closedProjection; + } + + /** + * Create a projecting variant of a mapped type. + * + * @param mappedType + * @param domainType + * @param properties + * @return + */ + public static EntityProjection projecting(TypeInformation mappedType, TypeInformation domainType, + List> properties, boolean closedProjection) { + return new EntityProjection<>(mappedType, domainType, properties, true, closedProjection); + } + + /** + * Create a non-projecting variant of a mapped type. + * + * @param mappedType + * @param domainType + * @param properties + * @return + */ + public static EntityProjection nonProjecting(TypeInformation mappedType, + TypeInformation domainType, + List> properties) { + return new EntityProjection<>(mappedType, domainType, properties, false, false); + } + + /** + * @return the mapped type used by this type view. + */ + public TypeInformation getMappedType() { + return mappedType; + } + + /** + * @return the actual mapped type used by this type view. Should be used for collection-like and map-like properties + * to determine the actual view type. + */ + public TypeInformation getActualMappedType() { + return mappedType.getRequiredActualType(); + } + + /** + * @return the domain type represented by this type view. + */ + public TypeInformation getDomainType() { + return domainType; + } + + /** + * @return the actual domain type represented by this type view. Should be used for collection-like and map-like + * properties to determine the actual domain type. + */ + public TypeInformation getActualDomainType() { + return domainType.getRequiredActualType(); + } + + /** + * @return {@code true} if the {@link #getMappedType()} is a projection. + */ + public boolean isProjection() { + return projection; + } + + /** + * @return {@code true} if the {@link #getMappedType()} is a closed projection. + */ + public boolean isClosedProjection() { + return isProjection() && closedProjection; + } + + List> getProperties() { + return properties; + } + + /** + * Perform the given {@code action} for each element of the {@code ReturnedTypeDescriptor} until all elements have + * been processed or the action throws an exception. + * + * @param action the action to be performed for each element + */ + public void forEach(Consumer action) { + + for (PropertyProjection descriptor : properties) { + + if (descriptor.getProperties().isEmpty()) { + action.accept(descriptor.getPropertyPath()); + } else { + descriptor.forEach(action); + } + } + } + + /** + * Return a {@link EntityProjection} for a property identified by {@code name}. + * + * @param name the property name. + * @return the type view, if the property is known; {@code null} otherwise. + */ + @Nullable + public EntityProjection findProperty(String name) { + + for (PropertyProjection descriptor : properties) { + + if (descriptor.propertyPath.getLeafProperty().getSegment().equals(name)) { + return descriptor; + } + } + + return null; + } + + @Override + public String toString() { + + if (isProjection()) { + return String.format("Projection(%s AS %s): %s", getActualDomainType().getType().getName(), + getActualMappedType().getType().getName(), properties); + } + + return String.format("Domain(%s): %s", getActualDomainType().getType().getName(), properties); + } + } + + /** + * Descriptor for a property-level type along its potential projection. + * + * @param the mapped type acting as view onto the domain type. + * @param the domain type. + */ + public static class PropertyProjection extends EntityProjection { + + private final PropertyPath propertyPath; + + PropertyProjection(PropertyPath propertyPath, TypeInformation mappedType, TypeInformation domainType, + List> properties, boolean projecting, boolean closedProjection) { + super(mappedType, domainType, properties, projecting, closedProjection); + this.propertyPath = propertyPath; + } + + /** + * Create a projecting variant of a mapped type. + * + * @param propertyPath + * @param mappedType + * @param domainType + * @param properties + * @return + */ + public static PropertyProjection projecting(PropertyPath propertyPath, TypeInformation mappedType, + TypeInformation domainType, List> properties, boolean closedProjection) { + return new PropertyProjection<>(propertyPath, mappedType, domainType, properties, true, closedProjection); + } + + /** + * Create a non-projecting variant of a mapped type. + * + * @param propertyPath + * @param mappedType + * @param domainType + * @return + */ + public static PropertyProjection nonProjecting(PropertyPath propertyPath, + TypeInformation mappedType, + TypeInformation domainType) { + return new PropertyProjection<>(propertyPath, mappedType, domainType, Collections.emptyList(), false, false); + } + + /** + * @return the property path representing this property within the root domain type. + */ + public PropertyPath getPropertyPath() { + return propertyPath; + } + + @Override + public String toString() { + return String.format("%s AS %s", propertyPath.toDotPath(), getActualMappedType().getType().getName()); + } + } + + /** + * Represents a predicate (boolean-valued function) of a {@link Class target type} and its {@link Class underlying + * type}. + */ + public interface ProjectionPredicate { + + /** + * Evaluates this predicate on the given arguments. + * + * @param target the target type. + * @param target the underlying type. + * @return {@code true} if the input argument matches the predicate, otherwise {@code false}. + */ + boolean test(Class target, Class underlyingType); + + /** + * Return a composed predicate that represents a short-circuiting logical AND of this predicate and another. When + * evaluating the composed predicate, if this predicate is {@code false}, then the {@code other} predicate is not + * evaluated. + *

+ * Any exceptions thrown during evaluation of either predicate are relayed to the caller; if evaluation of this + * predicate throws an exception, the {@code other} predicate will not be evaluated. + * + * @param other a predicate that will be logically-ANDed with this predicate + * @return a composed predicate that represents the short-circuiting logical AND of this predicate and the + * {@code other} predicate + */ + default ProjectionPredicate and(ProjectionPredicate other) { + return (target, underlyingType) -> test(target, underlyingType) && other.test(target, underlyingType); + } + + /** + * Return a predicate that represents the logical negation of this predicate. + * + * @return a predicate that represents the logical negation of this predicate + */ + default ProjectionPredicate negate() { + return (target, underlyingType) -> !test(target, underlyingType); + } + + /** + * Return a predicate that considers whether the {@code target type} is participating in the type hierarchy. + */ + static ProjectionPredicate typeHierarchy() { + + ProjectionPredicate predicate = (target, underlyingType) -> target.isAssignableFrom(underlyingType) || // hierarchy + underlyingType.isAssignableFrom(target); + return predicate.negate(); + } + + } + + static class CycleGuard { + Set> seen = new LinkedHashSet<>(); + + public boolean isCycleFree(PersistentProperty property) { + return seen.add(property); + } + } + +} diff --git a/src/test/java/org/springframework/data/mapping/context/EntityProjectionDiscovererUnitTests.java b/src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java similarity index 53% rename from src/test/java/org/springframework/data/mapping/context/EntityProjectionDiscovererUnitTests.java rename to src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java index ccc97f58e..b7bf46d6b 100644 --- a/src/test/java/org/springframework/data/mapping/context/EntityProjectionDiscovererUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java @@ -25,42 +25,42 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.mapping.context.EntityProjectionDiscoverer.ReturnedTypeDescriptor; +import org.springframework.data.mapping.context.EntityProjectionIntrospector.EntityProjection; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; /** - * Unit tests for {@link EntityProjectionDiscoverer}. + * Unit tests for {@link EntityProjectionIntrospector}. * * @author Mark Paluch */ -class EntityProjectionDiscovererUnitTests { +class EntityProjectionIntrospectorUnitTests { SampleMappingContext mappingContext = new SampleMappingContext(); SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); - EntityProjectionDiscoverer.ProjectionPredicate predicate = (target, + EntityProjectionIntrospector.ProjectionPredicate predicate = (target, underlyingType) -> !SimpleTypeHolder.DEFAULT.isSimpleType(target); - EntityProjectionDiscoverer discoverer = EntityProjectionDiscoverer.create(projectionFactory, - predicate.and(EntityProjectionDiscoverer.ProjectionPredicate.typeHierarchy()), mappingContext); + EntityProjectionIntrospector discoverer = EntityProjectionIntrospector.create(projectionFactory, + predicate.and(EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()), mappingContext); - @Test + @Test // GH-2420 void shouldDiscoverTypeHierarchy() { // super type - assertThat(discoverer.introspectReturnType(Root.class, Middle.class).isProjecting()).isFalse(); + assertThat(discoverer.introspect(Root.class, Middle.class).isProjection()).isFalse(); - assertThat(discoverer.introspectReturnType(SuperInterface.class, Middle.class).isProjecting()).isFalse(); + assertThat(discoverer.introspect(SuperInterface.class, Middle.class).isProjection()).isFalse(); // subtypes - assertThat(discoverer.introspectReturnType(Leaf.class, Middle.class).isProjecting()).isFalse(); + assertThat(discoverer.introspect(Leaf.class, Middle.class).isProjection()).isFalse(); } - @Test + @Test // GH-2420 void shouldConsiderTopLevelInterfaceProperties() { - ReturnedTypeDescriptor descriptor = discoverer.introspectReturnType(DomainClassProjection.class, DomainClass.class); + EntityProjection descriptor = discoverer.introspect(DomainClassProjection.class, DomainClass.class); - assertThat(descriptor.isProjecting()).isTrue(); + assertThat(descriptor.isProjection()).isTrue(); List paths = new ArrayList<>(); descriptor.forEach(paths::add); @@ -68,12 +68,12 @@ class EntityProjectionDiscovererUnitTests { assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("id", "value"); } - @Test + @Test // GH-2420 void shouldConsiderTopLevelDtoProperties() { - ReturnedTypeDescriptor descriptor = discoverer.introspectReturnType(DomainClassDto.class, DomainClass.class); + EntityProjection descriptor = discoverer.introspect(DomainClassDto.class, DomainClass.class); - assertThat(descriptor.isProjecting()).isTrue(); + assertThat(descriptor.isProjection()).isTrue(); List paths = new ArrayList<>(); descriptor.forEach(paths::add); @@ -81,13 +81,13 @@ class EntityProjectionDiscovererUnitTests { assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("id", "value"); } - @Test + @Test // GH-2420 void shouldConsiderNestedProjectionProperties() { - ReturnedTypeDescriptor descriptor = discoverer.introspectReturnType(WithNestedProjection.class, + EntityProjection descriptor = discoverer.introspect(WithNestedProjection.class, WithComplexObject.class); - assertThat(descriptor.isProjecting()).isTrue(); + assertThat(descriptor.isProjection()).isTrue(); List paths = new ArrayList<>(); descriptor.forEach(paths::add); @@ -96,12 +96,12 @@ class EntityProjectionDiscovererUnitTests { "domain2"); } - @Test + @Test // GH-2420 void shouldConsiderOpenProjection() { - ReturnedTypeDescriptor descriptor = discoverer.introspectReturnType(OpenProjection.class, DomainClass.class); + EntityProjection descriptor = discoverer.introspect(OpenProjection.class, DomainClass.class); - assertThat(descriptor.isProjecting()).isTrue(); + assertThat(descriptor.isProjection()).isTrue(); List paths = new ArrayList<>(); descriptor.forEach(paths::add); @@ -109,18 +109,34 @@ class EntityProjectionDiscovererUnitTests { assertThat(paths).isEmpty(); } - @Test - void shouldConsiderCyclicProjections() { + @Test // GH-2420 + void shouldConsiderCyclicPaths() { - ReturnedTypeDescriptor descriptor = discoverer.introspectReturnType(CyclicProjection1.class, CyclicDomain1.class); + EntityProjection descriptor = discoverer.introspect(PersonProjection.class, Person.class); - assertThat(descriptor.isProjecting()).isTrue(); + assertThat(descriptor.isProjection()).isTrue(); List paths = new ArrayList<>(); descriptor.forEach(paths::add); - assertThat(paths).hasSize(4).extracting(PropertyPath::toDotPath).containsOnly("name", "level1.name", - "level1.level2.name", "level1.level2.level1"); + // cycles are tracked on a per-property root basis. Global tracking would not expand "secondaryAddress" into its + // components. + assertThat(paths).extracting(PropertyPath::toDotPath).containsOnly("primaryAddress.owner.primaryAddress", + "primaryAddress.owner.secondaryAddress.owner", "secondaryAddress.owner.primaryAddress.owner", + "secondaryAddress.owner.secondaryAddress"); + } + + @Test // GH-2420 + void shouldConsiderCollectionProjection() { + + EntityProjection descriptor = discoverer.introspect(WithCollectionProjection.class, WithCollection.class); + + assertThat(descriptor.isProjection()).isTrue(); + + List paths = new ArrayList<>(); + descriptor.forEach(paths::add); + + assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("domains.id", "domains.value"); } interface SuperInterface { @@ -139,6 +155,16 @@ class EntityProjectionDiscovererUnitTests { } + static class WithCollection { + + List domains; + } + + interface WithCollectionProjection { + + List getDomains(); + } + static class DomainClass { String id; @@ -186,30 +212,27 @@ class EntityProjectionDiscovererUnitTests { } } - static class CyclicDomain1 { + static class Person { - CyclicDomain2 level1; - String name; + Address primaryAddress; + + Address secondaryAddress; } - static class CyclicDomain2 { + static class Address { - CyclicDomain1 level2; - String name; + Person owner; } - static interface CyclicProjection1 { + interface PersonProjection { - CyclicProjection2 getLevel1(); + AddressProjection getPrimaryAddress(); - String getName(); + AddressProjection getSecondaryAddress(); } - static interface CyclicProjection2 { + interface AddressProjection { - CyclicProjection1 getLevel2(); - - String getName(); + PersonProjection getOwner(); } - }