Skip to content

Commit ee6d501

Browse files
committed
Retain Field Lookup Policy when exposing aggregation fields.
Introduce FieldLookupPolicy and methods to create field-exposing/inheriting AggregationOperationContexts. Move off RelaxedTypeBasedAggregationOperationContext. See #4714 Original pull request: #4720
1 parent f5b7a38 commit ee6d501

14 files changed

+223
-167
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java

+11-26
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@
1616
package org.springframework.data.mongodb.core;
1717

1818
import java.util.List;
19-
import java.util.Optional;
20-
import java.util.stream.Collectors;
2119

2220
import org.bson.Document;
21+
2322
import org.springframework.data.mapping.context.MappingContext;
2423
import org.springframework.data.mongodb.core.aggregation.Aggregation;
2524
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
2625
import org.springframework.data.mongodb.core.aggregation.AggregationOptions.DomainTypeMapping;
27-
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
26+
import org.springframework.data.mongodb.core.aggregation.FieldLookupPolicy;
2827
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
2928
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
3029
import org.springframework.data.mongodb.core.convert.QueryMapper;
@@ -52,8 +51,8 @@ class AggregationUtil {
5251

5352
this.queryMapper = queryMapper;
5453
this.mappingContext = mappingContext;
55-
this.untypedMappingContext = Lazy
56-
.of(() -> new RelaxedTypeBasedAggregationOperationContext(Object.class, mappingContext, queryMapper));
54+
this.untypedMappingContext = Lazy.of(() -> new TypeBasedAggregationOperationContext(Object.class, mappingContext,
55+
queryMapper, FieldLookupPolicy.relaxed()));
5756
}
5857

5958
AggregationOperationContext createAggregationContext(Aggregation aggregation, @Nullable Class<?> inputType) {
@@ -64,27 +63,18 @@ AggregationOperationContext createAggregationContext(Aggregation aggregation, @N
6463
return Aggregation.DEFAULT_CONTEXT;
6564
}
6665

67-
if (!(aggregation instanceof TypedAggregation)) {
68-
69-
if(inputType == null) {
70-
return untypedMappingContext.get();
71-
}
72-
73-
if (domainTypeMapping == DomainTypeMapping.STRICT
74-
&& !aggregation.getPipeline().containsUnionWith()) {
75-
return new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
76-
}
66+
FieldLookupPolicy lookupPolicy = domainTypeMapping == DomainTypeMapping.STRICT
67+
&& !aggregation.getPipeline().containsUnionWith() ? FieldLookupPolicy.strict() : FieldLookupPolicy.relaxed();
7768

78-
return new RelaxedTypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
69+
if (aggregation instanceof TypedAggregation<?> ta) {
70+
return new TypeBasedAggregationOperationContext(ta.getInputType(), mappingContext, queryMapper, lookupPolicy);
7971
}
8072

81-
inputType = ((TypedAggregation<?>) aggregation).getInputType();
82-
if (domainTypeMapping == DomainTypeMapping.STRICT
83-
&& !aggregation.getPipeline().containsUnionWith()) {
84-
return new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
73+
if (inputType == null) {
74+
return untypedMappingContext.get();
8575
}
8676

87-
return new RelaxedTypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
77+
return new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper, lookupPolicy);
8878
}
8979

9080
/**
@@ -109,9 +99,4 @@ Document createCommand(String collection, Aggregation aggregation, AggregationOp
10999
return aggregation.toDocument(collection, context);
110100
}
111101

112-
private List<Document> mapAggregationPipeline(List<Document> pipeline) {
113-
114-
return pipeline.stream().map(val -> queryMapper.getMappedObject(val, Optional.empty()))
115-
.collect(Collectors.toList());
116-
}
117102
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java

+35-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
*
3636
* @author Oliver Gierke
3737
* @author Christoph Strobl
38+
* @author Mark Paluch
3839
* @since 1.3
3940
*/
4041
public interface AggregationOperationContext extends CodecRegistryProvider {
@@ -107,14 +108,46 @@ default Fields getFields(Class<?> type) {
107108
.toArray(String[]::new));
108109
}
109110

111+
/**
112+
* Create a nested {@link AggregationOperationContext} from this context that exposes {@link ExposedFields fields}.
113+
* <p>
114+
* Implementations of {@link AggregationOperationContext} retain their {@link FieldLookupPolicy}. If no policy is
115+
* specified, then lookup defaults to {@link FieldLookupPolicy#strict()}.
116+
*
117+
* @param fields the fields to expose, must not be {@literal null}.
118+
* @return the new {@link AggregationOperationContext} exposing {@code fields}.
119+
* @since 4.3.1
120+
*/
121+
default AggregationOperationContext expose(ExposedFields fields) {
122+
return new ExposedFieldsAggregationOperationContext(fields, this, FieldLookupPolicy.strict());
123+
}
124+
125+
/**
126+
* Create a nested {@link AggregationOperationContext} from this context that inherits exposed fields from this
127+
* context and exposes {@link ExposedFields fields}.
128+
* <p>
129+
* Implementations of {@link AggregationOperationContext} retain their {@link FieldLookupPolicy}. If no policy is
130+
* specified, then lookup defaults to {@link FieldLookupPolicy#strict()}.
131+
*
132+
* @param fields the fields to expose, must not be {@literal null}.
133+
* @return the new {@link AggregationOperationContext} exposing {@code fields}.
134+
* @since 4.3.1
135+
*/
136+
default AggregationOperationContext inheritAndExpose(ExposedFields fields) {
137+
return new InheritingExposedFieldsAggregationOperationContext(fields, this, FieldLookupPolicy.strict());
138+
}
139+
110140
/**
111141
* This toggle allows the {@link AggregationOperationContext context} to use any given field name without checking for
112-
* its existence. Typically the {@link AggregationOperationContext} fails when referencing unknown fields, those that
142+
* its existence. Typically, the {@link AggregationOperationContext} fails when referencing unknown fields, those that
113143
* are not present in one of the previous stages or the input source, throughout the pipeline.
114144
*
115145
* @return a more relaxed {@link AggregationOperationContext}.
116146
* @since 3.0
147+
* @deprecated since 4.3.1, {@link FieldLookupPolicy} should be specified explicitly when creating the
148+
* AggregationOperationContext.
117149
*/
150+
@Deprecated(since = "4.3.1", forRemoval = true)
118151
default AggregationOperationContext continueOnMissingFieldReference() {
119152
return this;
120153
}
@@ -123,4 +156,5 @@ default AggregationOperationContext continueOnMissingFieldReference() {
123156
default CodecRegistry getCodecRegistry() {
124157
return MongoClientSettings.getDefaultCodecRegistry();
125158
}
159+
126160
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ static List<Document> toDocument(List<AggregationOperation> operations, Aggregat
5050
List<Document> operationDocuments = new ArrayList<Document>(operations.size());
5151

5252
AggregationOperationContext contextToUse = rootContext;
53-
boolean relaxed = rootContext instanceof RelaxedTypeBasedAggregationOperationContext;
5453

5554
for (AggregationOperation operation : operations) {
5655

@@ -61,10 +60,10 @@ static List<Document> toDocument(List<AggregationOperation> operations, Aggregat
6160
ExposedFields fields = exposedFieldsOperation.getFields();
6261

6362
if (operation instanceof InheritsFieldsAggregationOperation || exposedFieldsOperation.inheritsFields()) {
64-
contextToUse = new InheritingExposedFieldsAggregationOperationContext(fields, contextToUse, relaxed);
63+
contextToUse = contextToUse.inheritAndExpose(fields);
6564
} else {
6665
contextToUse = fields.exposesNoFields() ? DEFAULT_CONTEXT
67-
: new ExposedFieldsAggregationOperationContext(fields, contextToUse, relaxed);
66+
: contextToUse.expose(fields);
6867
}
6968
}
7069

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -687,8 +687,7 @@ public Document toDocument(final AggregationOperationContext context) {
687687
private Document toFilter(ExposedFields exposedFields, AggregationOperationContext context) {
688688

689689
Document filterExpression = new Document();
690-
InheritingExposedFieldsAggregationOperationContext operationContext = new InheritingExposedFieldsAggregationOperationContext(
691-
exposedFields, context, false);
690+
AggregationOperationContext operationContext = context.inheritAndExpose(exposedFields);
692691

693692
filterExpression.putAll(context.getMappedObject(new Document("input", getMappedInput(context))));
694693
filterExpression.put("as", as.getTarget());

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ protected DocumentEnhancingOperation(Map<Object, Object> source) {
4949
@Override
5050
public Document toDocument(AggregationOperationContext context) {
5151

52-
InheritingExposedFieldsAggregationOperationContext operationContext = new InheritingExposedFieldsAggregationOperationContext(
53-
exposedFields, context, false);
52+
AggregationOperationContext operationContext = context.inheritAndExpose(exposedFields);
5453

5554
if (valueMap.size() == 1) {
5655
return context.getMappedObject(

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java

+29-13
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import org.bson.Document;
1919
import org.bson.codecs.configuration.CodecRegistry;
20+
2021
import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference;
2122
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
2223
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
@@ -37,24 +38,26 @@ class ExposedFieldsAggregationOperationContext implements AggregationOperationCo
3738

3839
private final ExposedFields exposedFields;
3940
private final AggregationOperationContext rootContext;
40-
private final boolean relaxedFieldLookup;
41+
private final FieldLookupPolicy lookupPolicy;
4142

4243
/**
4344
* Creates a new {@link ExposedFieldsAggregationOperationContext} from the given {@link ExposedFields}. Uses the given
4445
* {@link AggregationOperationContext} to perform a mapping to mongo types if necessary.
4546
*
4647
* @param exposedFields must not be {@literal null}.
4748
* @param rootContext must not be {@literal null}.
49+
* @param lookupPolicy must not be {@literal null}.
4850
*/
49-
public ExposedFieldsAggregationOperationContext(ExposedFields exposedFields,
50-
AggregationOperationContext rootContext, boolean relaxedFieldLookup) {
51+
public ExposedFieldsAggregationOperationContext(ExposedFields exposedFields, AggregationOperationContext rootContext,
52+
FieldLookupPolicy lookupPolicy) {
5153

5254
Assert.notNull(exposedFields, "ExposedFields must not be null");
5355
Assert.notNull(rootContext, "RootContext must not be null");
56+
Assert.notNull(lookupPolicy, "FieldLookupPolicy must not be null");
5457

5558
this.exposedFields = exposedFields;
5659
this.rootContext = rootContext;
57-
this.relaxedFieldLookup = relaxedFieldLookup;
60+
this.lookupPolicy = lookupPolicy;
5861
}
5962

6063
@Override
@@ -89,7 +92,7 @@ public Fields getFields(Class<?> type) {
8992
* @param name must not be {@literal null}.
9093
* @return
9194
*/
92-
protected FieldReference getReference(@Nullable Field field, String name) {
95+
private FieldReference getReference(@Nullable Field field, String name) {
9396

9497
Assert.notNull(name, "Name must not be null");
9598

@@ -98,14 +101,15 @@ protected FieldReference getReference(@Nullable Field field, String name) {
98101
return exposedField;
99102
}
100103

101-
if(relaxedFieldLookup) {
102-
if (field != null) {
103-
return new DirectFieldReference(new ExposedField(field, true));
104-
}
105-
return new DirectFieldReference(new ExposedField(name, true));
104+
if (lookupPolicy.isStrict()) {
105+
throw new IllegalArgumentException(String.format("Invalid reference '%s'", name));
106106
}
107107

108-
throw new IllegalArgumentException(String.format("Invalid reference '%s'", name));
108+
if (field != null) {
109+
return new DirectFieldReference(new ExposedField(field, true));
110+
}
111+
112+
return new DirectFieldReference(new ExposedField(name, true));
109113
}
110114

111115
/**
@@ -158,10 +162,22 @@ public CodecRegistry getCodecRegistry() {
158162
}
159163

160164
@Override
165+
@Deprecated(since = "4.3.1", forRemoval = true)
161166
public AggregationOperationContext continueOnMissingFieldReference() {
162-
if(relaxedFieldLookup) {
167+
if (!lookupPolicy.isStrict()) {
163168
return this;
164169
}
165-
return new ExposedFieldsAggregationOperationContext(exposedFields, rootContext, true);
170+
return new ExposedFieldsAggregationOperationContext(exposedFields, rootContext, FieldLookupPolicy.relaxed());
166171
}
172+
173+
@Override
174+
public AggregationOperationContext expose(ExposedFields fields) {
175+
return new ExposedFieldsAggregationOperationContext(fields, this, lookupPolicy);
176+
}
177+
178+
@Override
179+
public AggregationOperationContext inheritAndExpose(ExposedFields fields) {
180+
return new InheritingExposedFieldsAggregationOperationContext(fields, this, lookupPolicy);
181+
}
182+
167183
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2024 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.mongodb.core.aggregation;
17+
18+
/**
19+
* Lookup policy for aggregation fields. Allows strict lookups that fail if the field is absent or relaxed ones that
20+
* pass-thru the requested field even if we have to assume that the field isn't present because of the limited scope of
21+
* our input.
22+
*
23+
* @author Mark Paluch
24+
* @since 4.3.1
25+
*/
26+
public abstract class FieldLookupPolicy {
27+
28+
private static final FieldLookupPolicy STRICT = new FieldLookupPolicy() {
29+
@Override
30+
boolean isStrict() {
31+
return true;
32+
}
33+
};
34+
35+
private static final FieldLookupPolicy RELAXED = new FieldLookupPolicy() {
36+
@Override
37+
boolean isStrict() {
38+
return false;
39+
}
40+
};
41+
42+
private FieldLookupPolicy() {}
43+
44+
/**
45+
* @return a relaxed lookup policy.
46+
*/
47+
public static FieldLookupPolicy relaxed() {
48+
return RELAXED;
49+
}
50+
51+
/**
52+
* @return a strict lookup policy.
53+
*/
54+
public static FieldLookupPolicy strict() {
55+
return STRICT;
56+
}
57+
58+
/**
59+
* @return {@code true} if the policy uses a strict lookup; {@code false} to allow references to fields that cannot be
60+
* determined to be exactly present.
61+
*/
62+
abstract boolean isStrict();
63+
64+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import org.bson.Document;
1919
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
20+
import org.springframework.lang.Nullable;
2021

2122
/**
2223
* {@link ExposedFieldsAggregationOperationContext} that inherits fields from its parent
@@ -36,11 +37,12 @@ class InheritingExposedFieldsAggregationOperationContext extends ExposedFieldsAg
3637
*
3738
* @param exposedFields must not be {@literal null}.
3839
* @param previousContext must not be {@literal null}.
40+
* @param lookupPolicy must not be {@literal null}.
3941
*/
4042
public InheritingExposedFieldsAggregationOperationContext(ExposedFields exposedFields,
41-
AggregationOperationContext previousContext, boolean continueOnMissingFieldReference) {
43+
AggregationOperationContext previousContext, FieldLookupPolicy lookupPolicy) {
4244

43-
super(exposedFields, previousContext, continueOnMissingFieldReference);
45+
super(exposedFields, previousContext, lookupPolicy);
4446

4547
this.previousContext = previousContext;
4648
}
@@ -51,7 +53,7 @@ public Document getMappedObject(Document document) {
5153
}
5254

5355
@Override
54-
protected FieldReference resolveExposedField(Field field, String name) {
56+
protected FieldReference resolveExposedField(@Nullable Field field, String name) {
5557

5658
FieldReference fieldReference = super.resolveExposedField(field, name);
5759
if (fieldReference != null) {

0 commit comments

Comments
 (0)