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();
}
-
}