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 9ea42b251..61e5c6873 100644 --- a/src/main/java/org/springframework/data/mapping/context/EntityProjection.java +++ b/src/main/java/org/springframework/data/mapping/context/EntityProjection.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mapping.context; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -23,6 +24,7 @@ import java.util.Map; import java.util.function.Consumer; import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; @@ -34,6 +36,8 @@ import org.springframework.lang.Nullable; * * @param the mapped type acting as view onto the domain type. * @param the domain type. + * @author Mark Paluch + * @author Christoph Strobl * @since 2.7 */ public class EntityProjection implements Streamable> { @@ -42,61 +46,63 @@ public class EntityProjection implements Streamable domainType; private final List> properties; private final boolean projection; - private final boolean closedProjection; + private final ProjectionType projectionType; EntityProjection(TypeInformation mappedType, TypeInformation domainType, - List> properties, boolean projection, boolean closedProjection) { + List> properties, boolean projection, ProjectionType projectionType) { this.mappedType = mappedType; this.domainType = domainType; - this.properties = properties; + this.properties = new ArrayList<>(properties); this.projection = projection; - this.closedProjection = closedProjection; + this.projectionType = projectionType; } /** * Create a projecting variant of a mapped type. * - * @param mappedType - * @param domainType - * @param properties - * @return + * @param mappedType the target projection type. Must not be {@literal null}. + * @param domainType the source domain type. Must not be {@literal null}. + * @param properties properties to include. + * @param projectionType must not be {@literal null}. + * @return new instance of {@link EntityProjection}. */ public static EntityProjection projecting(TypeInformation mappedType, TypeInformation domainType, - List> properties, boolean closedProjection) { - return new EntityProjection<>(mappedType, domainType, properties, true, closedProjection); + List> properties, ProjectionType projectionType) { + return new EntityProjection<>(mappedType, domainType, properties, true, projectionType); } /** * Create a non-projecting variant of a mapped type. * - * @param mappedType - * @param domainType - * @param properties - * @return + * @param mappedType the target projection type. Must not be {@literal null}. + * @param domainType the source domain type. Must not be {@literal null}. + * @param properties properties to include. + * @return new instance of {@link EntityProjection}. */ public static EntityProjection nonProjecting(TypeInformation mappedType, TypeInformation domainType, List> properties) { - return new EntityProjection<>(mappedType, domainType, properties, false, false); + return new EntityProjection<>(mappedType, domainType, properties, false, ProjectionType.CLOSED); } /** * Create a non-projecting variant of a {@code type}. * - * @param type - * @return + * @param type must not be {@literal null}. + * @return new instance of {@link EntityProjection}. */ public static EntityProjection nonProjecting(Class type) { ClassTypeInformation typeInformation = ClassTypeInformation.from(type); - return new EntityProjection<>(typeInformation, typeInformation, Collections.emptyList(), false, false); + return new EntityProjection<>(typeInformation, typeInformation, Collections.emptyList(), false, + ProjectionType.CLOSED); } /** * 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. + * have been processed or the action throws an {@link 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 + * @param action must not be {@literal null}. */ public void forEachRecursive(Consumer> action) { @@ -128,6 +134,7 @@ public class EntityProjection implements Streamable getActualMappedType() { return mappedType.getRequiredActualType(); @@ -143,6 +150,7 @@ public class EntityProjection implements Streamable getActualDomainType() { return domainType.getRequiredActualType(); @@ -159,7 +167,8 @@ public class EntityProjection implements Streamable> getProperties() { @@ -207,36 +216,38 @@ public class EntityProjection implements Streamable mappedType, TypeInformation domainType, - List> properties, boolean projecting, boolean closedProjection) { - super(mappedType, domainType, properties, projecting, closedProjection); + List> properties, boolean projecting, ProjectionType projectionType) { + super(mappedType, domainType, properties, projecting, projectionType); this.propertyPath = propertyPath; } /** * Create a projecting variant of a mapped type. * - * @param propertyPath - * @param mappedType - * @param domainType - * @param properties - * @return + * @param propertyPath the {@link PropertyPath path} to the actual property. + * @param mappedType the target projection type. Must not be {@literal null}. + * @param domainType the source domain type. Must not be {@literal null}. + * @param properties properties to include. + * @param projectionType must not be {@literal null}. + * @return new instance of {@link PropertyProjection}. */ public static PropertyProjection projecting(PropertyPath propertyPath, TypeInformation mappedType, - TypeInformation domainType, List> properties, boolean closedProjection) { - return new PropertyProjection<>(propertyPath, mappedType, domainType, properties, true, closedProjection); + TypeInformation domainType, List> properties, ProjectionType projectionType) { + return new PropertyProjection<>(propertyPath, mappedType, domainType, properties, true, projectionType); } /** * Create a non-projecting variant of a mapped type. * - * @param propertyPath - * @param mappedType - * @param domainType + * @param propertyPath the {@link PropertyPath path} to the actual property. + * @param mappedType the target projection type. Must not be {@literal null}. + * @param domainType the source domain type. Must not be {@literal null}. * @return */ public static PropertyProjection nonProjecting(PropertyPath propertyPath, TypeInformation mappedType, TypeInformation domainType) { - return new PropertyProjection<>(propertyPath, mappedType, domainType, Collections.emptyList(), false, false); + return new PropertyProjection<>(propertyPath, mappedType, domainType, Collections.emptyList(), false, + ProjectionType.OPEN); } /** @@ -264,39 +275,79 @@ public class EntityProjection implements Streamable extends PropertyProjection { ContainerPropertyProjection(PropertyPath propertyPath, TypeInformation mappedType, TypeInformation domainType, - List> properties, boolean projecting, boolean closedProjection) { - super(propertyPath, mappedType, domainType, properties, projecting, closedProjection); + List> properties, boolean projecting, ProjectionType projectionType) { + super(propertyPath, mappedType, domainType, properties, projecting, projectionType); } /** * Create a projecting variant of a mapped type. * - * @param propertyPath - * @param mappedType - * @param domainType - * @param properties - * @return + * @param propertyPath the {@link PropertyPath path} to the actual property. + * @param mappedType the target projection type. Must not be {@literal null}. + * @param domainType the source domain type. Must not be {@literal null}. + * @param properties properties to include. + * @param projectionType must not be {@literal null}. + * @return new instance of {@link ContainerPropertyProjection}. */ public static ContainerPropertyProjection projecting(PropertyPath propertyPath, TypeInformation mappedType, TypeInformation domainType, List> properties, - boolean closedProjection) { - return new ContainerPropertyProjection<>(propertyPath, mappedType, domainType, properties, true, - closedProjection); + ProjectionType projectionType) { + return new ContainerPropertyProjection<>(propertyPath, mappedType, domainType, properties, true, projectionType); } /** * Create a non-projecting variant of a mapped type. * - * @param propertyPath - * @param mappedType - * @param domainType - * @return + * @param propertyPath the {@link PropertyPath path} to the actual property. + * @param mappedType the target projection type. Must not be {@literal null}. + * @param domainType the source domain type. Must not be {@literal null}. + * @return new instance of {@link ContainerPropertyProjection}. */ public static ContainerPropertyProjection nonProjecting(PropertyPath propertyPath, TypeInformation mappedType, TypeInformation domainType) { return new ContainerPropertyProjection<>(propertyPath, mappedType, domainType, Collections.emptyList(), false, - false); + ProjectionType.OPEN); } } + + /** + * Projection type. + * + * @since 2.7 + */ + public enum ProjectionType { + + /** + * A DTO projection defines a value type that hold properties for the fields that are supposed to be retrieved. + */ + DTO, + + /** + * An open projection has accessor methods in the interface that can be used to compute new values by using the + * {@link org.springframework.beans.factory.annotation.Value} annotation. + */ + OPEN, + + /** + * A closed projection only contains accessor methods that all match properties of the target aggregate. + */ + CLOSED; + + /** + * Obtain the {@link ProjectionType} from a given {@link ProjectionInformation}. + * + * @param information must not be {@literal null}. + * @return the {@link ProjectionType} according to {@link ProjectionInformation#getType() type} and + * {@link ProjectionInformation#isClosed()}. + */ + public static ProjectionType from(ProjectionInformation information) { + + if (!information.getType().isInterface()) { + return DTO; + } + + return information.isClosed() ? CLOSED : OPEN; + } + } } 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 b6935887c..5b51dca5d 100644 --- a/src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java +++ b/src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java @@ -25,6 +25,7 @@ import java.util.Set; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.context.EntityProjection.ProjectionType; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.util.ClassTypeInformation; @@ -38,6 +39,7 @@ import org.springframework.util.Assert; * * @author Gerrit Meier * @author Mark Paluch + * @author Christoph Strobl * @since 2.7 */ public class EntityProjectionIntrospector { @@ -48,6 +50,7 @@ public class EntityProjectionIntrospector { private EntityProjectionIntrospector(ProjectionFactory projectionFactory, ProjectionPredicate projectionPredicate, MappingContext mappingContext) { + this.projectionFactory = projectionFactory; this.projectionPredicate = projectionPredicate; this.mappingContext = mappingContext; @@ -102,14 +105,14 @@ public class EntityProjectionIntrospector { if (!projectionInformation.isClosed()) { return EntityProjection.projecting(returnedTypeInformation, domainTypeInformation, Collections.emptyList(), - false); + ProjectionType.OPEN); } PersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(domainType); List> propertyDescriptors = getProperties(null, projectionInformation, returnedTypeInformation, persistentEntity, null); - return EntityProjection.projecting(returnedTypeInformation, domainTypeInformation, propertyDescriptors, true); + return EntityProjection.projecting(returnedTypeInformation, domainTypeInformation, propertyDescriptors, ProjectionType.CLOSED); } private List> getProperties(@Nullable PropertyPath propertyPath, @@ -138,7 +141,7 @@ public class EntityProjectionIntrospector { ? PropertyPath.from(persistentProperty.getName(), persistentEntity.getTypeInformation()) : propertyPath.nested(persistentProperty.getName()); - TypeInformation unwrappedReturnedType = unwrapContainerType(property.getRequiredActualType()); + TypeInformation unwrappedReturnedType = unwrapContainerType(actualType); TypeInformation unwrappedDomainType = unwrapContainerType( persistentProperty.getTypeInformation().getRequiredActualType()); @@ -155,10 +158,10 @@ public class EntityProjectionIntrospector { if (container) { propertyDescriptors.add(EntityProjection.ContainerPropertyProjection.projecting(nestedPropertyPath, property, - persistentProperty.getTypeInformation(), nestedPropertyDescriptors, projectionInformation.isClosed())); + persistentProperty.getTypeInformation(), nestedPropertyDescriptors, ProjectionType.from(projectionInformation))); } else { propertyDescriptors.add(EntityProjection.PropertyProjection.projecting(nestedPropertyPath, property, - persistentProperty.getTypeInformation(), nestedPropertyDescriptors, projectionInformation.isClosed())); + persistentProperty.getTypeInformation(), nestedPropertyDescriptors, ProjectionType.from(projectionInformation))); } } else { @@ -216,7 +219,7 @@ public class EntityProjectionIntrospector { * Evaluates this predicate on the given arguments. * * @param target the target type. - * @param target the underlying type. + * @param underlyingType the underlying type. * @return {@code true} if the input argument matches the predicate, otherwise {@code false}. */ boolean test(Class target, Class underlyingType); 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 97a21d3fc..5a1254249 100644 --- a/src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java @@ -34,6 +34,7 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; * Unit tests for {@link EntityProjectionIntrospector}. * * @author Mark Paluch + * @author Christoph Strobl */ class EntityProjectionIntrospectorUnitTests { @@ -62,6 +63,7 @@ class EntityProjectionIntrospectorUnitTests { EntityProjection descriptor = discoverer.introspect(DomainClassProjection.class, DomainClass.class); assertThat(descriptor.isProjection()).isTrue(); + assertThat(descriptor.isClosedProjection()).isTrue(); List paths = new ArrayList<>(); descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath())); @@ -75,6 +77,7 @@ class EntityProjectionIntrospectorUnitTests { EntityProjection descriptor = discoverer.introspect(DomainClassDto.class, DomainClass.class); assertThat(descriptor.isProjection()).isTrue(); + assertThat(descriptor.isClosedProjection()).isTrue(); List paths = new ArrayList<>(); descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath())); @@ -103,6 +106,7 @@ class EntityProjectionIntrospectorUnitTests { EntityProjection descriptor = discoverer.introspect(OpenProjection.class, DomainClass.class); assertThat(descriptor.isProjection()).isTrue(); + assertThat(descriptor.isClosedProjection()).isFalse(); List paths = new ArrayList<>(); descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));