diff --git a/src/main/java/org/springframework/data/mapping/context/EntityProjection.java b/src/main/java/org/springframework/data/mapping/context/EntityProjection.java index 1cdf1c91d..9ea42b251 100644 --- a/src/main/java/org/springframework/data/mapping/context/EntityProjection.java +++ b/src/main/java/org/springframework/data/mapping/context/EntityProjection.java @@ -15,12 +15,16 @@ */ package org.springframework.data.mapping.context; +import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -32,7 +36,7 @@ import org.springframework.lang.Nullable; * @param the domain type. * @since 2.7 */ -public class EntityProjection { +public class EntityProjection implements Streamable> { private final TypeInformation mappedType; private final TypeInformation domainType; @@ -86,6 +90,34 @@ public class EntityProjection { return new EntityProjection<>(typeInformation, typeInformation, Collections.emptyList(), false, false); } + /** + * Performs the given action for each element of the {@link Streamable} recursively until all elements of the graph + * have been processed or the action throws an exception. Unless otherwise specified by the implementing class, + * actions are performed in the order of iteration (if an iteration order is specified). Exceptions thrown by the + * action are relayed to the caller. + * + * @param action + */ + public void forEachRecursive(Consumer> action) { + + for (PropertyProjection descriptor : properties) { + + if (descriptor instanceof ContainerPropertyProjection) { + action.accept(descriptor); + descriptor.forEachRecursive(action); + } else if (descriptor.getProperties().isEmpty()) { + action.accept(descriptor); + } else { + descriptor.forEachRecursive(action); + } + } + } + + @Override + public Iterator> iterator() { + return properties.iterator(); + } + /** * @return the mapped type used by this type view. */ @@ -134,24 +166,6 @@ public class EntityProjection { 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}. * @@ -236,5 +250,53 @@ public class EntityProjection { public String toString() { return String.format("%s AS %s", propertyPath.toDotPath(), getActualMappedType().getType().getName()); } + + } + + /** + * Descriptor for a property-level type along its potential projection that is held within a {@link Collection}-like + * or {@link Map}-like container. Property paths within containers use the deeply unwrapped actual type of the + * container as root type and as they cannot be tied immediately to the root entity. + * + * @param the mapped type acting as view onto the domain type. + * @param the domain type. + */ + public static class ContainerPropertyProjection extends PropertyProjection { + + ContainerPropertyProjection(PropertyPath propertyPath, TypeInformation mappedType, TypeInformation domainType, + List> properties, boolean projecting, boolean closedProjection) { + super(propertyPath, mappedType, domainType, properties, projecting, closedProjection); + } + + /** + * Create a projecting variant of a mapped type. + * + * @param propertyPath + * @param mappedType + * @param domainType + * @param properties + * @return + */ + public static ContainerPropertyProjection projecting(PropertyPath propertyPath, + TypeInformation mappedType, TypeInformation domainType, List> properties, + boolean closedProjection) { + return new ContainerPropertyProjection<>(propertyPath, mappedType, domainType, properties, true, + closedProjection); + } + + /** + * Create a non-projecting variant of a mapped type. + * + * @param propertyPath + * @param mappedType + * @param domainType + * @return + */ + public static ContainerPropertyProjection nonProjecting(PropertyPath propertyPath, + TypeInformation mappedType, TypeInformation domainType) { + return new ContainerPropertyProjection<>(propertyPath, mappedType, domainType, Collections.emptyList(), false, + false); + } + } } diff --git a/src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java b/src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java index 7b03f1c77..b6935887c 100644 --- a/src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java +++ b/src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java @@ -78,10 +78,14 @@ public class EntityProjectionIntrospector { *

* Nested properties (direct types, within maps, collections) are introspected for nested projections and contain * property paths for closed projections. + *

+ * Deeply nested types (e.g. {@code Map<?, List<Person>>}) are represented with a property path that uses + * the unwrapped type and no longer the root domain type {@code D}. * - * @param mappedType - * @param domainType - * @return + * @param mappedType must not be {@literal null}. + * @param domainType must not be {@literal null}. + * @return the introspection result. + * @see org.springframework.data.mapping.context.EntityProjection.ContainerPropertyProjection */ public EntityProjection introspect(Class mappedType, Class domainType) { @@ -103,8 +107,7 @@ public class EntityProjectionIntrospector { PersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(domainType); List> propertyDescriptors = getProperties(null, projectionInformation, - returnedTypeInformation, - persistentEntity, null); + returnedTypeInformation, persistentEntity, null); return EntityProjection.projecting(returnedTypeInformation, domainTypeInformation, propertyDescriptors, true); } @@ -127,44 +130,71 @@ public class EntityProjectionIntrospector { CycleGuard cycleGuardToUse = cycleGuard != null ? cycleGuard : new CycleGuard(); TypeInformation property = projectionTypeInformation.getRequiredProperty(inputProperty.getName()); + TypeInformation actualType = property.getRequiredActualType(); + + boolean container = isContainer(actualType); PropertyPath nestedPropertyPath = propertyPath == null ? PropertyPath.from(persistentProperty.getName(), persistentEntity.getTypeInformation()) : propertyPath.nested(persistentProperty.getName()); - TypeInformation returnedType = property.getRequiredActualType(); - TypeInformation domainType = persistentProperty.getTypeInformation().getRequiredActualType(); + TypeInformation unwrappedReturnedType = unwrapContainerType(property.getRequiredActualType()); + TypeInformation unwrappedDomainType = unwrapContainerType( + persistentProperty.getTypeInformation().getRequiredActualType()); - if (isProjection(returnedType, domainType)) { + if (isProjection(unwrappedReturnedType, unwrappedDomainType)) { List> nestedPropertyDescriptors; if (cycleGuardToUse.isCycleFree(persistentProperty)) { - nestedPropertyDescriptors = getProjectedProperties(nestedPropertyPath, returnedType, domainType, - cycleGuardToUse); + nestedPropertyDescriptors = getProjectedProperties(container ? null : nestedPropertyPath, + unwrappedReturnedType, unwrappedDomainType, cycleGuardToUse); } else { nestedPropertyDescriptors = Collections.emptyList(); } - propertyDescriptors.add(EntityProjection.PropertyProjection.projecting(nestedPropertyPath, property, - persistentProperty.getTypeInformation(), - nestedPropertyDescriptors, projectionInformation.isClosed())); + if (container) { + propertyDescriptors.add(EntityProjection.ContainerPropertyProjection.projecting(nestedPropertyPath, property, + persistentProperty.getTypeInformation(), nestedPropertyDescriptors, projectionInformation.isClosed())); + } else { + propertyDescriptors.add(EntityProjection.PropertyProjection.projecting(nestedPropertyPath, property, + persistentProperty.getTypeInformation(), nestedPropertyDescriptors, projectionInformation.isClosed())); + } + } else { - propertyDescriptors - .add(EntityProjection.PropertyProjection.nonProjecting(nestedPropertyPath, property, - persistentProperty.getTypeInformation())); + if (container) { + propertyDescriptors.add(EntityProjection.ContainerPropertyProjection.nonProjecting(nestedPropertyPath, + property, persistentProperty.getTypeInformation())); + } else { + propertyDescriptors.add(EntityProjection.PropertyProjection.nonProjecting(nestedPropertyPath, property, + persistentProperty.getTypeInformation())); + } } } return propertyDescriptors; } + private static TypeInformation unwrapContainerType(TypeInformation type) { + + TypeInformation unwrapped = type; + while (isContainer(unwrapped)) { + unwrapped = unwrapped.getRequiredActualType(); + } + + return unwrapped; + } + + private static boolean isContainer(TypeInformation actualType) { + return actualType.isCollectionLike() || actualType.isMap(); + } + private boolean isProjection(TypeInformation returnedType, TypeInformation domainType) { return projectionPredicate.test(returnedType.getRequiredActualType().getType(), domainType.getRequiredActualType().getType()); } - private List> getProjectedProperties(PropertyPath propertyPath, + private List> getProjectedProperties(@Nullable PropertyPath propertyPath, TypeInformation returnedType, TypeInformation domainType, CycleGuard cycleGuard) { ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnedType.getType()); diff --git a/src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java b/src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java index 2b3061d60..97a21d3fc 100644 --- a/src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java @@ -17,10 +17,12 @@ package org.springframework.data.mapping.context; import static org.assertj.core.api.Assertions.*; +import lombok.Getter; import lombok.Value; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; @@ -62,7 +64,7 @@ class EntityProjectionIntrospectorUnitTests { assertThat(descriptor.isProjection()).isTrue(); List paths = new ArrayList<>(); - descriptor.forEach(paths::add); + descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath())); assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("id", "value"); } @@ -75,7 +77,7 @@ class EntityProjectionIntrospectorUnitTests { assertThat(descriptor.isProjection()).isTrue(); List paths = new ArrayList<>(); - descriptor.forEach(paths::add); + descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath())); assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("id", "value"); } @@ -89,7 +91,7 @@ class EntityProjectionIntrospectorUnitTests { assertThat(descriptor.isProjection()).isTrue(); List paths = new ArrayList<>(); - descriptor.forEach(paths::add); + descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath())); assertThat(paths).hasSize(3).extracting(PropertyPath::toDotPath).containsOnly("domain.id", "domain.value", "domain2"); @@ -103,7 +105,7 @@ class EntityProjectionIntrospectorUnitTests { assertThat(descriptor.isProjection()).isTrue(); List paths = new ArrayList<>(); - descriptor.forEach(paths::add); + descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath())); assertThat(paths).isEmpty(); } @@ -116,7 +118,7 @@ class EntityProjectionIntrospectorUnitTests { assertThat(descriptor.isProjection()).isTrue(); List paths = new ArrayList<>(); - descriptor.forEach(paths::add); + descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath())); // cycles are tracked on a per-property root basis. Global tracking would not expand "secondaryAddress" into its // components. @@ -133,11 +135,27 @@ class EntityProjectionIntrospectorUnitTests { assertThat(descriptor.isProjection()).isTrue(); List paths = new ArrayList<>(); - descriptor.forEach(paths::add); + descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath())); assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("domains.id", "domains.value"); } + @Test // GH-2420 + void considersPropertiesWithinContainers() { + + EntityProjection descriptor = discoverer.introspect(WithMapOfCollectionProjection.class, + WithMapOfCollection.class); + + assertThat(descriptor.isProjection()).isTrue(); + + List paths = new ArrayList<>(); + descriptor.forEachRecursive(it -> { + paths.add(it.getPropertyPath()); + }); + + assertThat(paths).hasSize(3).extracting(PropertyPath::toDotPath).containsOnly("domains", "id", "value"); + } + interface SuperInterface { } @@ -159,6 +177,17 @@ class EntityProjectionIntrospectorUnitTests { List domains; } + static class WithMapOfCollection { + + Map> domains; + } + + @Getter + static class WithMapOfCollectionProjection { + + Map> domains; + } + interface WithCollectionProjection { List getDomains();