Skip to content

Commit 9948fef

Browse files
mp911dechristophstrobl
authored andcommitted
Provide class for inspecting nested projections.
Original Pull Request: #2420
1 parent 8969a55 commit 9948fef

File tree

2 files changed

+569
-0
lines changed

2 files changed

+569
-0
lines changed
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mapping.context;
17+
18+
import java.beans.PropertyDescriptor;
19+
import java.util.ArrayList;
20+
import java.util.Collections;
21+
import java.util.HashSet;
22+
import java.util.List;
23+
import java.util.Set;
24+
import java.util.function.Consumer;
25+
26+
import org.springframework.data.mapping.PersistentEntity;
27+
import org.springframework.data.mapping.PersistentProperty;
28+
import org.springframework.data.mapping.PropertyPath;
29+
import org.springframework.data.projection.ProjectionFactory;
30+
import org.springframework.data.projection.ProjectionInformation;
31+
import org.springframework.data.util.Pair;
32+
import org.springframework.lang.Nullable;
33+
import org.springframework.util.Assert;
34+
35+
/**
36+
* This class is introspects the returned type in the context of a domain type for all reachable properties (w/o cycles)
37+
* to determine which property paths are subject to projection.
38+
*
39+
* @author Gerrit Meier
40+
* @author Mark Paluch
41+
* @since 2.7
42+
*/
43+
public class EntityProjectionDiscoverer {
44+
45+
private final ProjectionFactory projectionFactory;
46+
private final ProjectionPredicate projectionPredicate;
47+
private final MappingContext<?, ?> mappingContext;
48+
49+
private EntityProjectionDiscoverer(ProjectionFactory projectionFactory, ProjectionPredicate projectionPredicate,
50+
MappingContext<?, ?> mappingContext) {
51+
this.projectionFactory = projectionFactory;
52+
this.projectionPredicate = projectionPredicate;
53+
this.mappingContext = mappingContext;
54+
}
55+
56+
/**
57+
* Create a new {@link EntityProjectionDiscoverer} given {@link ProjectionFactory}, {@link ProjectionPredicate} and
58+
* {@link MappingContext}.
59+
*
60+
* @param projectionFactory must not be {@literal null}.
61+
* @param projectionPredicate must not be {@literal null}.
62+
* @param mappingContext must not be {@literal null}.
63+
* @return a new {@link EntityProjectionDiscoverer} instance.
64+
*/
65+
public static EntityProjectionDiscoverer create(ProjectionFactory projectionFactory,
66+
ProjectionPredicate projectionPredicate, MappingContext<?, ?> mappingContext) {
67+
68+
Assert.notNull(projectionFactory, "ProjectionFactory must not be null");
69+
Assert.notNull(projectionPredicate, "ProjectionPredicate must not be null");
70+
Assert.notNull(mappingContext, "MappingContext must not be null");
71+
72+
return new EntityProjectionDiscoverer(projectionFactory, projectionPredicate, mappingContext);
73+
}
74+
75+
/**
76+
* Introspect a {@link Class return type} in the context of a {@link Class domain type} whether the returned type is a
77+
* projection and what property paths are participating in the projection.
78+
* <p>
79+
* Nested properties (direct types, within maps, collections) are introspected for nested projections and contain
80+
* property paths for closed projections.
81+
*
82+
* @param returnType
83+
* @param domainType
84+
* @return
85+
*/
86+
public ReturnedTypeDescriptor introspectReturnType(Class<?> returnType, Class<?> domainType) {
87+
88+
boolean isProjection = projectionPredicate.test(returnType, domainType);
89+
90+
if (!isProjection) {
91+
return ReturnedTypeDescriptor.nonProjecting(returnType, domainType, Collections.emptyList());
92+
}
93+
94+
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType);
95+
96+
if (!projectionInformation.isClosed()) {
97+
return ReturnedTypeDescriptor.projecting(returnType, domainType, Collections.emptyList());
98+
}
99+
100+
Set<Pair<Class<?>, Class<?>>> cycleGuard = new HashSet<>();
101+
102+
PersistentEntity<?, ?> persistentEntity = mappingContext.getRequiredPersistentEntity(domainType);
103+
List<PropertyProjectionDescriptor> propertyDescriptors = getProperties(null, projectionInformation,
104+
persistentEntity, cycleGuard);
105+
106+
return ReturnedTypeDescriptor.projecting(returnType, domainType, propertyDescriptors);
107+
}
108+
109+
private List<PropertyProjectionDescriptor> getProperties(@Nullable PropertyPath propertyPath,
110+
ProjectionInformation projectionInformation, PersistentEntity<?, ?> persistentEntity,
111+
Set<Pair<Class<?>, Class<?>>> cycleGuard) {
112+
113+
List<PropertyProjectionDescriptor> propertyDescriptors = new ArrayList<>();
114+
for (PropertyDescriptor inputProperty : projectionInformation.getInputProperties()) {
115+
116+
PersistentProperty<?> persistentProperty = persistentEntity.getPersistentProperty(inputProperty.getName());
117+
118+
if (persistentProperty == null) {
119+
continue;
120+
}
121+
122+
Class<?> returnedType = inputProperty.getPropertyType();
123+
Class<?> domainType = persistentProperty.getActualType();
124+
125+
PropertyPath nestedPropertyPath = propertyPath == null
126+
? PropertyPath.from(persistentProperty.getName(), persistentEntity.getTypeInformation())
127+
: propertyPath.nested(persistentProperty.getName());
128+
129+
if (projectionPredicate.test(returnedType, domainType)) {
130+
131+
List<PropertyProjectionDescriptor> nestedPropertyDescriptors;
132+
133+
if (cycleGuard.add(Pair.of(returnedType, domainType))) {
134+
nestedPropertyDescriptors = getProjectedProperties(nestedPropertyPath, returnedType, domainType, cycleGuard);
135+
} else {
136+
nestedPropertyDescriptors = Collections.emptyList();
137+
}
138+
139+
propertyDescriptors.add(PropertyProjectionDescriptor.projecting(nestedPropertyPath, returnedType, domainType,
140+
nestedPropertyDescriptors));
141+
} else {
142+
propertyDescriptors
143+
.add(PropertyProjectionDescriptor.nonProjecting(nestedPropertyPath, returnedType, domainType));
144+
}
145+
}
146+
147+
return propertyDescriptors;
148+
}
149+
150+
private List<PropertyProjectionDescriptor> getProjectedProperties(PropertyPath propertyPath, Class<?> returnedType,
151+
Class<?> domainType, Set<Pair<Class<?>, Class<?>>> cycleGuard) {
152+
153+
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnedType);
154+
PersistentEntity<?, ?> persistentEntity = mappingContext.getRequiredPersistentEntity(domainType);
155+
156+
// Closed projection should get handled as above (recursion)
157+
return projectionInformation.isClosed()
158+
? getProperties(propertyPath, projectionInformation, persistentEntity, cycleGuard)
159+
: Collections.emptyList();
160+
}
161+
162+
/**
163+
* Descriptor for a top-level return type.
164+
*/
165+
public static class ReturnedTypeDescriptor {
166+
167+
private final Class<?> returnedType;
168+
private final Class<?> domainType;
169+
private final List<PropertyProjectionDescriptor> nested;
170+
private final boolean projecting;
171+
172+
ReturnedTypeDescriptor(Class<?> returnedType, Class<?> domainType, List<PropertyProjectionDescriptor> nested,
173+
boolean projecting) {
174+
this.domainType = domainType;
175+
this.returnedType = returnedType;
176+
this.nested = nested;
177+
this.projecting = projecting;
178+
}
179+
180+
/**
181+
* Create a projecting variant of a return type.
182+
*
183+
* @param returnedType
184+
* @param domainType
185+
* @param nested
186+
* @return
187+
*/
188+
public static ReturnedTypeDescriptor projecting(Class<?> returnedType, Class<?> domainType,
189+
List<PropertyProjectionDescriptor> nested) {
190+
return new ReturnedTypeDescriptor(returnedType, domainType, nested, true);
191+
}
192+
193+
/**
194+
* Create a non-projecting variant of a return type.
195+
*
196+
* @param returnedType
197+
* @param domainType
198+
* @param nested
199+
* @return
200+
*/
201+
public static ReturnedTypeDescriptor nonProjecting(Class<?> returnedType, Class<?> domainType,
202+
List<PropertyProjectionDescriptor> nested) {
203+
return new ReturnedTypeDescriptor(returnedType, domainType, nested, false);
204+
}
205+
206+
public Class<?> getDomainType() {
207+
return domainType;
208+
}
209+
210+
public Class<?> getReturnedType() {
211+
return returnedType;
212+
}
213+
214+
public boolean isProjecting() {
215+
return projecting;
216+
}
217+
218+
List<PropertyProjectionDescriptor> getNested() {
219+
return nested;
220+
}
221+
222+
/**
223+
* Perform the given {@code action} for each element of the {@code ReturnedTypeDescriptor} until all elements have
224+
* been processed or the action throws an exception.
225+
*
226+
* @param action the action to be performed for each element
227+
*/
228+
public void forEach(Consumer<PropertyPath> action) {
229+
230+
for (PropertyProjectionDescriptor descriptor : nested) {
231+
232+
if (descriptor.getNested().isEmpty()) {
233+
action.accept(descriptor.getPropertyPath());
234+
} else {
235+
descriptor.forEach(action);
236+
}
237+
}
238+
}
239+
240+
@Override
241+
public String toString() {
242+
243+
if (isProjecting()) {
244+
return String.format("Projection(%s AS %s): %s", getDomainType().getName(), getReturnedType().getName(),
245+
nested);
246+
}
247+
248+
return String.format("Domain(%s): %s", getReturnedType().getName(), nested);
249+
}
250+
}
251+
252+
/**
253+
* Descriptor for a property-level type along its potential projection.
254+
*/
255+
public static class PropertyProjectionDescriptor extends ReturnedTypeDescriptor {
256+
257+
private final PropertyPath propertyPath;
258+
259+
PropertyProjectionDescriptor(PropertyPath propertyPath, Class<?> returnedType, Class<?> domainType,
260+
List<PropertyProjectionDescriptor> nested, boolean projecting) {
261+
super(returnedType, domainType, nested, projecting);
262+
this.propertyPath = propertyPath;
263+
}
264+
265+
/**
266+
* Create a projecting variant of a return type.
267+
*
268+
* @param propertyPath
269+
* @param returnedType
270+
* @param domainType
271+
* @param nested
272+
* @return
273+
*/
274+
public static PropertyProjectionDescriptor projecting(PropertyPath propertyPath, Class<?> returnedType,
275+
Class<?> domainType, List<PropertyProjectionDescriptor> nested) {
276+
return new PropertyProjectionDescriptor(propertyPath, returnedType, domainType, nested, true);
277+
}
278+
279+
/**
280+
* Create a non-projecting variant of a return type.
281+
*
282+
* @param propertyPath
283+
* @param returnedType
284+
* @param domainType
285+
* @return
286+
*/
287+
public static PropertyProjectionDescriptor nonProjecting(PropertyPath propertyPath, Class<?> returnedType,
288+
Class<?> domainType) {
289+
return new PropertyProjectionDescriptor(propertyPath, returnedType, domainType, Collections.emptyList(), false);
290+
}
291+
292+
public PropertyPath getPropertyPath() {
293+
return propertyPath;
294+
}
295+
296+
@Override
297+
public String toString() {
298+
return String.format("%s AS %s", propertyPath.toDotPath(), getReturnedType().getName());
299+
}
300+
}
301+
302+
/**
303+
* Represents a predicate (boolean-valued function) of a {@link Class target type} and its {@link Class underlying
304+
* type}.
305+
*/
306+
public interface ProjectionPredicate {
307+
308+
/**
309+
* Evaluates this predicate on the given arguments.
310+
*
311+
* @param target the target type.
312+
* @param target the underlying type.
313+
* @return {@code true} if the input argument matches the predicate, otherwise {@code false}.
314+
*/
315+
boolean test(Class<?> target, Class<?> underlyingType);
316+
317+
/**
318+
* Return a composed predicate that represents a short-circuiting logical AND of this predicate and another. When
319+
* evaluating the composed predicate, if this predicate is {@code false}, then the {@code other} predicate is not
320+
* evaluated.
321+
* <p>
322+
* Any exceptions thrown during evaluation of either predicate are relayed to the caller; if evaluation of this
323+
* predicate throws an exception, the {@code other} predicate will not be evaluated.
324+
*
325+
* @param other a predicate that will be logically-ANDed with this predicate
326+
* @return a composed predicate that represents the short-circuiting logical AND of this predicate and the
327+
* {@code other} predicate
328+
*/
329+
default ProjectionPredicate and(ProjectionPredicate other) {
330+
return (target, underlyingType) -> test(target, underlyingType) && other.test(target, underlyingType);
331+
}
332+
333+
/**
334+
* Return a predicate that represents the logical negation of this predicate.
335+
*
336+
* @return a predicate that represents the logical negation of this predicate
337+
*/
338+
default ProjectionPredicate negate() {
339+
return (target, underlyingType) -> !test(target, underlyingType);
340+
}
341+
342+
/**
343+
* Return a predicate that considers whether the {@code target type} is participating in the type hierarchy.
344+
*/
345+
static ProjectionPredicate typeHierarchy() {
346+
347+
ProjectionPredicate predicate = (target, underlyingType) -> target.isAssignableFrom(underlyingType) || // hierarchy
348+
underlyingType.isAssignableFrom(target);
349+
return predicate.negate();
350+
}
351+
352+
}
353+
354+
}

0 commit comments

Comments
 (0)