Skip to content

Commit a63774e

Browse files
mp911dechristophstrobl
authored andcommitted
Add support for properties using deep map-in-map/list-in-map nesting.
Original Pull Request: #2420
1 parent 0744580 commit a63774e

File tree

3 files changed

+163
-42
lines changed

3 files changed

+163
-42
lines changed

src/main/java/org/springframework/data/mapping/context/EntityProjection.java

+81-19
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@
1515
*/
1616
package org.springframework.data.mapping.context;
1717

18+
import java.util.Collection;
1819
import java.util.Collections;
20+
import java.util.Iterator;
1921
import java.util.List;
22+
import java.util.Map;
2023
import java.util.function.Consumer;
2124

2225
import org.springframework.data.mapping.PropertyPath;
2326
import org.springframework.data.util.ClassTypeInformation;
27+
import org.springframework.data.util.Streamable;
2428
import org.springframework.data.util.TypeInformation;
2529
import org.springframework.lang.Nullable;
2630

@@ -32,7 +36,7 @@
3236
* @param <D> the domain type.
3337
* @since 2.7
3438
*/
35-
public class EntityProjection<M, D> {
39+
public class EntityProjection<M, D> implements Streamable<EntityProjection.PropertyProjection<?, ?>> {
3640

3741
private final TypeInformation<M> mappedType;
3842
private final TypeInformation<D> domainType;
@@ -86,6 +90,34 @@ public static <T> EntityProjection<T, T> nonProjecting(Class<T> type) {
8690
return new EntityProjection<>(typeInformation, typeInformation, Collections.emptyList(), false, false);
8791
}
8892

93+
/**
94+
* Performs the given action for each element of the {@link Streamable} recursively until all elements of the graph
95+
* have been processed or the action throws an exception. Unless otherwise specified by the implementing class,
96+
* actions are performed in the order of iteration (if an iteration order is specified). Exceptions thrown by the
97+
* action are relayed to the caller.
98+
*
99+
* @param action
100+
*/
101+
public void forEachRecursive(Consumer<? super PropertyProjection<?, ?>> action) {
102+
103+
for (PropertyProjection<?, ?> descriptor : properties) {
104+
105+
if (descriptor instanceof ContainerPropertyProjection) {
106+
action.accept(descriptor);
107+
descriptor.forEachRecursive(action);
108+
} else if (descriptor.getProperties().isEmpty()) {
109+
action.accept(descriptor);
110+
} else {
111+
descriptor.forEachRecursive(action);
112+
}
113+
}
114+
}
115+
116+
@Override
117+
public Iterator<PropertyProjection<?, ?>> iterator() {
118+
return properties.iterator();
119+
}
120+
89121
/**
90122
* @return the mapped type used by this type view.
91123
*/
@@ -134,24 +166,6 @@ public boolean isClosedProjection() {
134166
return properties;
135167
}
136168

137-
/**
138-
* Perform the given {@code action} for each element of the {@code ReturnedTypeDescriptor} until all elements have
139-
* been processed or the action throws an exception.
140-
*
141-
* @param action the action to be performed for each element
142-
*/
143-
public void forEach(Consumer<PropertyPath> action) {
144-
145-
for (PropertyProjection<?, ?> descriptor : properties) {
146-
147-
if (descriptor.getProperties().isEmpty()) {
148-
action.accept(descriptor.getPropertyPath());
149-
} else {
150-
descriptor.forEach(action);
151-
}
152-
}
153-
}
154-
155169
/**
156170
* Return a {@link EntityProjection} for a property identified by {@code name}.
157171
*
@@ -236,5 +250,53 @@ public PropertyPath getPropertyPath() {
236250
public String toString() {
237251
return String.format("%s AS %s", propertyPath.toDotPath(), getActualMappedType().getType().getName());
238252
}
253+
254+
}
255+
256+
/**
257+
* Descriptor for a property-level type along its potential projection that is held within a {@link Collection}-like
258+
* or {@link Map}-like container. Property paths within containers use the deeply unwrapped actual type of the
259+
* container as root type and as they cannot be tied immediately to the root entity.
260+
*
261+
* @param <M> the mapped type acting as view onto the domain type.
262+
* @param <D> the domain type.
263+
*/
264+
public static class ContainerPropertyProjection<M, D> extends PropertyProjection<M, D> {
265+
266+
ContainerPropertyProjection(PropertyPath propertyPath, TypeInformation<M> mappedType, TypeInformation<D> domainType,
267+
List<PropertyProjection<?, ?>> properties, boolean projecting, boolean closedProjection) {
268+
super(propertyPath, mappedType, domainType, properties, projecting, closedProjection);
269+
}
270+
271+
/**
272+
* Create a projecting variant of a mapped type.
273+
*
274+
* @param propertyPath
275+
* @param mappedType
276+
* @param domainType
277+
* @param properties
278+
* @return
279+
*/
280+
public static <M, D> ContainerPropertyProjection<M, D> projecting(PropertyPath propertyPath,
281+
TypeInformation<M> mappedType, TypeInformation<D> domainType, List<PropertyProjection<?, ?>> properties,
282+
boolean closedProjection) {
283+
return new ContainerPropertyProjection<>(propertyPath, mappedType, domainType, properties, true,
284+
closedProjection);
285+
}
286+
287+
/**
288+
* Create a non-projecting variant of a mapped type.
289+
*
290+
* @param propertyPath
291+
* @param mappedType
292+
* @param domainType
293+
* @return
294+
*/
295+
public static <M, D> ContainerPropertyProjection<M, D> nonProjecting(PropertyPath propertyPath,
296+
TypeInformation<M> mappedType, TypeInformation<D> domainType) {
297+
return new ContainerPropertyProjection<>(propertyPath, mappedType, domainType, Collections.emptyList(), false,
298+
false);
299+
}
300+
239301
}
240302
}

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

+47-17
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,14 @@ public static EntityProjectionIntrospector create(ProjectionFactory projectionFa
7878
* <p>
7979
* Nested properties (direct types, within maps, collections) are introspected for nested projections and contain
8080
* property paths for closed projections.
81+
* <p>
82+
* Deeply nested types (e.g. {@code Map&lt;?, List&lt;Person&gt;&gt;}) are represented with a property path that uses
83+
* the unwrapped type and no longer the root domain type {@code D}.
8184
*
82-
* @param mappedType
83-
* @param domainType
84-
* @return
85+
* @param mappedType must not be {@literal null}.
86+
* @param domainType must not be {@literal null}.
87+
* @return the introspection result.
88+
* @see org.springframework.data.mapping.context.EntityProjection.ContainerPropertyProjection
8589
*/
8690
public <M, D> EntityProjection<M, D> introspect(Class<M> mappedType, Class<D> domainType) {
8791

@@ -103,8 +107,7 @@ public <M, D> EntityProjection<M, D> introspect(Class<M> mappedType, Class<D> do
103107

104108
PersistentEntity<?, ?> persistentEntity = mappingContext.getRequiredPersistentEntity(domainType);
105109
List<EntityProjection.PropertyProjection<?, ?>> propertyDescriptors = getProperties(null, projectionInformation,
106-
returnedTypeInformation,
107-
persistentEntity, null);
110+
returnedTypeInformation, persistentEntity, null);
108111

109112
return EntityProjection.projecting(returnedTypeInformation, domainTypeInformation, propertyDescriptors, true);
110113
}
@@ -127,44 +130,71 @@ public <M, D> EntityProjection<M, D> introspect(Class<M> mappedType, Class<D> do
127130
CycleGuard cycleGuardToUse = cycleGuard != null ? cycleGuard : new CycleGuard();
128131

129132
TypeInformation<?> property = projectionTypeInformation.getRequiredProperty(inputProperty.getName());
133+
TypeInformation<?> actualType = property.getRequiredActualType();
134+
135+
boolean container = isContainer(actualType);
130136

131137
PropertyPath nestedPropertyPath = propertyPath == null
132138
? PropertyPath.from(persistentProperty.getName(), persistentEntity.getTypeInformation())
133139
: propertyPath.nested(persistentProperty.getName());
134140

135-
TypeInformation<?> returnedType = property.getRequiredActualType();
136-
TypeInformation<?> domainType = persistentProperty.getTypeInformation().getRequiredActualType();
141+
TypeInformation<?> unwrappedReturnedType = unwrapContainerType(property.getRequiredActualType());
142+
TypeInformation<?> unwrappedDomainType = unwrapContainerType(
143+
persistentProperty.getTypeInformation().getRequiredActualType());
137144

138-
if (isProjection(returnedType, domainType)) {
145+
if (isProjection(unwrappedReturnedType, unwrappedDomainType)) {
139146

140147
List<EntityProjection.PropertyProjection<?, ?>> nestedPropertyDescriptors;
141148

142149
if (cycleGuardToUse.isCycleFree(persistentProperty)) {
143-
nestedPropertyDescriptors = getProjectedProperties(nestedPropertyPath, returnedType, domainType,
144-
cycleGuardToUse);
150+
nestedPropertyDescriptors = getProjectedProperties(container ? null : nestedPropertyPath,
151+
unwrappedReturnedType, unwrappedDomainType, cycleGuardToUse);
145152
} else {
146153
nestedPropertyDescriptors = Collections.emptyList();
147154
}
148155

149-
propertyDescriptors.add(EntityProjection.PropertyProjection.projecting(nestedPropertyPath, property,
150-
persistentProperty.getTypeInformation(),
151-
nestedPropertyDescriptors, projectionInformation.isClosed()));
156+
if (container) {
157+
propertyDescriptors.add(EntityProjection.ContainerPropertyProjection.projecting(nestedPropertyPath, property,
158+
persistentProperty.getTypeInformation(), nestedPropertyDescriptors, projectionInformation.isClosed()));
159+
} else {
160+
propertyDescriptors.add(EntityProjection.PropertyProjection.projecting(nestedPropertyPath, property,
161+
persistentProperty.getTypeInformation(), nestedPropertyDescriptors, projectionInformation.isClosed()));
162+
}
163+
152164
} else {
153-
propertyDescriptors
154-
.add(EntityProjection.PropertyProjection.nonProjecting(nestedPropertyPath, property,
155-
persistentProperty.getTypeInformation()));
165+
if (container) {
166+
propertyDescriptors.add(EntityProjection.ContainerPropertyProjection.nonProjecting(nestedPropertyPath,
167+
property, persistentProperty.getTypeInformation()));
168+
} else {
169+
propertyDescriptors.add(EntityProjection.PropertyProjection.nonProjecting(nestedPropertyPath, property,
170+
persistentProperty.getTypeInformation()));
171+
}
156172
}
157173
}
158174

159175
return propertyDescriptors;
160176
}
161177

178+
private static TypeInformation<?> unwrapContainerType(TypeInformation<?> type) {
179+
180+
TypeInformation<?> unwrapped = type;
181+
while (isContainer(unwrapped)) {
182+
unwrapped = unwrapped.getRequiredActualType();
183+
}
184+
185+
return unwrapped;
186+
}
187+
188+
private static boolean isContainer(TypeInformation<?> actualType) {
189+
return actualType.isCollectionLike() || actualType.isMap();
190+
}
191+
162192
private boolean isProjection(TypeInformation<?> returnedType, TypeInformation<?> domainType) {
163193
return projectionPredicate.test(returnedType.getRequiredActualType().getType(),
164194
domainType.getRequiredActualType().getType());
165195
}
166196

167-
private List<EntityProjection.PropertyProjection<?, ?>> getProjectedProperties(PropertyPath propertyPath,
197+
private List<EntityProjection.PropertyProjection<?, ?>> getProjectedProperties(@Nullable PropertyPath propertyPath,
168198
TypeInformation<?> returnedType, TypeInformation<?> domainType, CycleGuard cycleGuard) {
169199

170200
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnedType.getType());

src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java

+35-6
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import lombok.Getter;
2021
import lombok.Value;
2122

2223
import java.util.ArrayList;
2324
import java.util.List;
25+
import java.util.Map;
2426

2527
import org.junit.jupiter.api.Test;
2628

@@ -62,7 +64,7 @@ void shouldConsiderTopLevelInterfaceProperties() {
6264
assertThat(descriptor.isProjection()).isTrue();
6365

6466
List<PropertyPath> paths = new ArrayList<>();
65-
descriptor.forEach(paths::add);
67+
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
6668

6769
assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("id", "value");
6870
}
@@ -75,7 +77,7 @@ void shouldConsiderTopLevelDtoProperties() {
7577
assertThat(descriptor.isProjection()).isTrue();
7678

7779
List<PropertyPath> paths = new ArrayList<>();
78-
descriptor.forEach(paths::add);
80+
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
7981

8082
assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("id", "value");
8183
}
@@ -89,7 +91,7 @@ void shouldConsiderNestedProjectionProperties() {
8991
assertThat(descriptor.isProjection()).isTrue();
9092

9193
List<PropertyPath> paths = new ArrayList<>();
92-
descriptor.forEach(paths::add);
94+
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
9395

9496
assertThat(paths).hasSize(3).extracting(PropertyPath::toDotPath).containsOnly("domain.id", "domain.value",
9597
"domain2");
@@ -103,7 +105,7 @@ void shouldConsiderOpenProjection() {
103105
assertThat(descriptor.isProjection()).isTrue();
104106

105107
List<PropertyPath> paths = new ArrayList<>();
106-
descriptor.forEach(paths::add);
108+
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
107109

108110
assertThat(paths).isEmpty();
109111
}
@@ -116,7 +118,7 @@ void shouldConsiderCyclicPaths() {
116118
assertThat(descriptor.isProjection()).isTrue();
117119

118120
List<PropertyPath> paths = new ArrayList<>();
119-
descriptor.forEach(paths::add);
121+
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
120122

121123
// cycles are tracked on a per-property root basis. Global tracking would not expand "secondaryAddress" into its
122124
// components.
@@ -133,11 +135,27 @@ void shouldConsiderCollectionProjection() {
133135
assertThat(descriptor.isProjection()).isTrue();
134136

135137
List<PropertyPath> paths = new ArrayList<>();
136-
descriptor.forEach(paths::add);
138+
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
137139

138140
assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("domains.id", "domains.value");
139141
}
140142

143+
@Test // GH-2420
144+
void considersPropertiesWithinContainers() {
145+
146+
EntityProjection<?, ?> descriptor = discoverer.introspect(WithMapOfCollectionProjection.class,
147+
WithMapOfCollection.class);
148+
149+
assertThat(descriptor.isProjection()).isTrue();
150+
151+
List<PropertyPath> paths = new ArrayList<>();
152+
descriptor.forEachRecursive(it -> {
153+
paths.add(it.getPropertyPath());
154+
});
155+
156+
assertThat(paths).hasSize(3).extracting(PropertyPath::toDotPath).containsOnly("domains", "id", "value");
157+
}
158+
141159
interface SuperInterface {
142160

143161
}
@@ -159,6 +177,17 @@ static class WithCollection {
159177
List<DomainClass> domains;
160178
}
161179

180+
static class WithMapOfCollection {
181+
182+
Map<String, List<DomainClass>> domains;
183+
}
184+
185+
@Getter
186+
static class WithMapOfCollectionProjection {
187+
188+
Map<String, List<DomainClassProjection>> domains;
189+
}
190+
162191
interface WithCollectionProjection {
163192

164193
List<DomainClassProjection> getDomains();

0 commit comments

Comments
 (0)