Browse Source

Refactor PropertyFilterSupport into EntityProjectionIntrospector.

Original Pull Request: #2420
pull/2524/head
Mark Paluch 4 years ago committed by Christoph Strobl
parent
commit
caa8c6afc5
No known key found for this signature in database
GPG Key ID: 8CC1AB53391458C8
  1. 354
      src/main/java/org/springframework/data/mapping/context/EntityProjectionDiscoverer.java
  2. 445
      src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java
  3. 107
      src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java

354
src/main/java/org/springframework/data/mapping/context/EntityProjectionDiscoverer.java

@ -1,354 +0,0 @@ @@ -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.
* <p>
* 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<Pair<Class<?>, Class<?>>> cycleGuard = new HashSet<>();
PersistentEntity<?, ?> persistentEntity = mappingContext.getRequiredPersistentEntity(domainType);
List<PropertyProjectionDescriptor> propertyDescriptors = getProperties(null, projectionInformation,
persistentEntity, cycleGuard);
return ReturnedTypeDescriptor.projecting(returnType, domainType, propertyDescriptors);
}
private List<PropertyProjectionDescriptor> getProperties(@Nullable PropertyPath propertyPath,
ProjectionInformation projectionInformation, PersistentEntity<?, ?> persistentEntity,
Set<Pair<Class<?>, Class<?>>> cycleGuard) {
List<PropertyProjectionDescriptor> 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<PropertyProjectionDescriptor> 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<PropertyProjectionDescriptor> getProjectedProperties(PropertyPath propertyPath, Class<?> returnedType,
Class<?> domainType, Set<Pair<Class<?>, 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<PropertyProjectionDescriptor> nested;
private final boolean projecting;
ReturnedTypeDescriptor(Class<?> returnedType, Class<?> domainType, List<PropertyProjectionDescriptor> 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<PropertyProjectionDescriptor> 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<PropertyProjectionDescriptor> nested) {
return new ReturnedTypeDescriptor(returnedType, domainType, nested, false);
}
public Class<?> getDomainType() {
return domainType;
}
public Class<?> getReturnedType() {
return returnedType;
}
public boolean isProjecting() {
return projecting;
}
List<PropertyProjectionDescriptor> 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<PropertyPath> 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<PropertyProjectionDescriptor> 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<PropertyProjectionDescriptor> 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.
* <p>
* 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();
}
}
}

445
src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java

@ -0,0 +1,445 @@ @@ -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.
* <p>
* 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 <M, D> EntityProjection<M, D> introspect(Class<M> mappedType, Class<D> domainType) {
ClassTypeInformation<M> returnedTypeInformation = ClassTypeInformation.from(mappedType);
ClassTypeInformation<D> 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<PropertyProjection<?, ?>> propertyDescriptors = getProperties(null, projectionInformation,
returnedTypeInformation,
persistentEntity, null);
return EntityProjection.projecting(returnedTypeInformation, domainTypeInformation, propertyDescriptors, true);
}
private List<PropertyProjection<?, ?>> getProperties(@Nullable PropertyPath propertyPath,
ProjectionInformation projectionInformation, TypeInformation<?> projectionTypeInformation,
PersistentEntity<?, ?> persistentEntity, @Nullable CycleGuard cycleGuard) {
List<PropertyProjection<?, ?>> 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<PropertyProjection<?, ?>> 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<PropertyProjection<?, ?>> 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 <M> the mapped type acting as view onto the domain type.
* @param <D> the domain type.
*/
public static class EntityProjection<M, D> {
private final TypeInformation<M> mappedType;
private final TypeInformation<D> domainType;
private final List<PropertyProjection<?, ?>> properties;
private final boolean projection;
private final boolean closedProjection;
EntityProjection(TypeInformation<M> mappedType, TypeInformation<D> domainType,
List<PropertyProjection<?, ?>> 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 <M, D> EntityProjection<M, D> projecting(TypeInformation<M> mappedType, TypeInformation<D> domainType,
List<PropertyProjection<?, ?>> 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 <M, D> EntityProjection<M, D> nonProjecting(TypeInformation<M> mappedType,
TypeInformation<D> domainType,
List<PropertyProjection<?, ?>> properties) {
return new EntityProjection<>(mappedType, domainType, properties, false, false);
}
/**
* @return the mapped type used by this type view.
*/
public TypeInformation<M> 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<D> 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<PropertyProjection<?, ?>> 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<PropertyPath> 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 <M> the mapped type acting as view onto the domain type.
* @param <D> the domain type.
*/
public static class PropertyProjection<M, D> extends EntityProjection<M, D> {
private final PropertyPath propertyPath;
PropertyProjection(PropertyPath propertyPath, TypeInformation<M> mappedType, TypeInformation<D> domainType,
List<PropertyProjection<?, ?>> 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 <M, D> PropertyProjection<M, D> projecting(PropertyPath propertyPath, TypeInformation<M> mappedType,
TypeInformation<D> domainType, List<PropertyProjection<?, ?>> 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 <M, D> PropertyProjection<M, D> nonProjecting(PropertyPath propertyPath,
TypeInformation<M> mappedType,
TypeInformation<D> 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.
* <p>
* 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<PersistentProperty<?>> seen = new LinkedHashSet<>();
public boolean isCycleFree(PersistentProperty<?> property) {
return seen.add(property);
}
}
}

107
src/test/java/org/springframework/data/mapping/context/EntityProjectionDiscovererUnitTests.java → src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java

@ -25,42 +25,42 @@ import java.util.List; @@ -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<PropertyPath> paths = new ArrayList<>();
descriptor.forEach(paths::add);
@ -68,12 +68,12 @@ class EntityProjectionDiscovererUnitTests { @@ -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<PropertyPath> paths = new ArrayList<>();
descriptor.forEach(paths::add);
@ -81,13 +81,13 @@ class EntityProjectionDiscovererUnitTests { @@ -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<PropertyPath> paths = new ArrayList<>();
descriptor.forEach(paths::add);
@ -96,12 +96,12 @@ class EntityProjectionDiscovererUnitTests { @@ -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<PropertyPath> paths = new ArrayList<>();
descriptor.forEach(paths::add);
@ -109,18 +109,34 @@ class EntityProjectionDiscovererUnitTests { @@ -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<PropertyPath> 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<PropertyPath> 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 { @@ -139,6 +155,16 @@ class EntityProjectionDiscovererUnitTests {
}
static class WithCollection {
List<DomainClass> domains;
}
interface WithCollectionProjection {
List<DomainClassProjection> getDomains();
}
static class DomainClass {
String id;
@ -186,30 +212,27 @@ class EntityProjectionDiscovererUnitTests { @@ -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();
}
}
Loading…
Cancel
Save