Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit cba4b9d

Browse files
committedJun 13, 2024··
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 daec5d8 commit cba4b9d

14 files changed

+223
-167
lines changed
 

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

Lines changed: 11 additions & 26 deletions
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

Lines changed: 35 additions & 1 deletion
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

Lines changed: 2 additions & 3 deletions
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

Lines changed: 1 addition & 2 deletions
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

Lines changed: 1 addition & 2 deletions
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

Lines changed: 29 additions & 13 deletions
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

Lines changed: 5 additions & 3 deletions
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) {

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

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,8 @@
1515
*/
1616
package org.springframework.data.mongodb.core.aggregation;
1717

18-
import org.springframework.data.mapping.MappingException;
1918
import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
2019
import org.springframework.data.mapping.context.MappingContext;
21-
import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference;
22-
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
23-
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
2420
import org.springframework.data.mongodb.core.convert.QueryMapper;
2521
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
2622
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@@ -31,7 +27,9 @@
3127
*
3228
* @author Christoph Strobl
3329
* @since 3.0
30+
* @deprecated since 4.3.1
3431
*/
32+
@Deprecated(since = "4.3.1")
3533
public class RelaxedTypeBasedAggregationOperationContext extends TypeBasedAggregationOperationContext {
3634

3735
/**
@@ -44,16 +42,6 @@ public class RelaxedTypeBasedAggregationOperationContext extends TypeBasedAggreg
4442
*/
4543
public RelaxedTypeBasedAggregationOperationContext(Class<?> type,
4644
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, QueryMapper mapper) {
47-
super(type, mappingContext, mapper);
48-
}
49-
50-
@Override
51-
protected FieldReference getReferenceFor(Field field) {
52-
53-
try {
54-
return super.getReferenceFor(field);
55-
} catch (MappingException e) {
56-
return new DirectFieldReference(new ExposedField(field, true));
57-
}
45+
super(type, mappingContext, mapper, FieldLookupPolicy.relaxed());
5846
}
5947
}

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

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
import java.util.List;
2222

2323
import org.bson.Document;
24-
2524
import org.bson.codecs.configuration.CodecRegistry;
25+
26+
import org.springframework.data.mapping.MappingException;
2627
import org.springframework.data.mapping.PersistentPropertyPath;
2728
import org.springframework.data.mapping.context.MappingContext;
2829
import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference;
@@ -50,6 +51,7 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio
5051
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
5152
private final QueryMapper mapper;
5253
private final Lazy<MongoPersistentEntity<?>> entity;
54+
private final FieldLookupPolicy lookupPolicy;
5355

5456
/**
5557
* Creates a new {@link TypeBasedAggregationOperationContext} for the given type, {@link MappingContext} and
@@ -61,15 +63,33 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio
6163
*/
6264
public TypeBasedAggregationOperationContext(Class<?> type,
6365
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, QueryMapper mapper) {
66+
this(type, mappingContext, mapper, FieldLookupPolicy.strict());
67+
}
68+
69+
/**
70+
* Creates a new {@link TypeBasedAggregationOperationContext} for the given type, {@link MappingContext} and
71+
* {@link QueryMapper}.
72+
*
73+
* @param type must not be {@literal null}.
74+
* @param mappingContext must not be {@literal null}.
75+
* @param mapper must not be {@literal null}.
76+
* @param lookupPolicy must not be {@literal null}.
77+
* @since 4.3.1
78+
*/
79+
public TypeBasedAggregationOperationContext(Class<?> type,
80+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, QueryMapper mapper,
81+
FieldLookupPolicy lookupPolicy) {
6482

6583
Assert.notNull(type, "Type must not be null");
6684
Assert.notNull(mappingContext, "MappingContext must not be null");
6785
Assert.notNull(mapper, "QueryMapper must not be null");
86+
Assert.notNull(lookupPolicy, "FieldLookupPolicy must not be null");
6887

6988
this.type = type;
7089
this.mappingContext = mappingContext;
7190
this.mapper = mapper;
7291
this.entity = Lazy.of(() -> mappingContext.getPersistentEntity(type));
92+
this.lookupPolicy = lookupPolicy;
7393
}
7494

7595
@Override
@@ -113,6 +133,7 @@ public Fields getFields(Class<?> type) {
113133
}
114134

115135
@Override
136+
@Deprecated(since = "4.3.1", forRemoval = true)
116137
public AggregationOperationContext continueOnMissingFieldReference() {
117138
return continueOnMissingFieldReference(type);
118139
}
@@ -128,19 +149,43 @@ public AggregationOperationContext continueOnMissingFieldReference() {
128149
* @see RelaxedTypeBasedAggregationOperationContext
129150
*/
130151
public AggregationOperationContext continueOnMissingFieldReference(Class<?> type) {
131-
return new RelaxedTypeBasedAggregationOperationContext(type, mappingContext, mapper);
152+
return new TypeBasedAggregationOperationContext(type, mappingContext, mapper, FieldLookupPolicy.relaxed());
153+
}
154+
155+
@Override
156+
public AggregationOperationContext expose(ExposedFields fields) {
157+
return new ExposedFieldsAggregationOperationContext(fields, this, lookupPolicy);
158+
}
159+
160+
@Override
161+
public AggregationOperationContext inheritAndExpose(ExposedFields fields) {
162+
return new InheritingExposedFieldsAggregationOperationContext(fields, this, lookupPolicy);
132163
}
133164

134165
protected FieldReference getReferenceFor(Field field) {
135166

136-
if(entity.getNullable() == null || AggregationVariable.isVariable(field)) {
167+
try {
168+
return doGetFieldReference(field);
169+
} catch (MappingException e) {
170+
171+
if (lookupPolicy.isStrict()) {
172+
throw e;
173+
}
174+
175+
return new DirectFieldReference(new ExposedField(field, true));
176+
}
177+
}
178+
179+
private DirectFieldReference doGetFieldReference(Field field) {
180+
181+
if (entity.getNullable() == null || AggregationVariable.isVariable(field)) {
137182
return new DirectFieldReference(new ExposedField(field, true));
138183
}
139184

140185
PersistentPropertyPath<MongoPersistentProperty> propertyPath = mappingContext
141-
.getPersistentPropertyPath(field.getTarget(), type);
186+
.getPersistentPropertyPath(field.getTarget(), type);
142187
Field mappedField = field(field.getName(),
143-
propertyPath.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE));
188+
propertyPath.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE));
144189

145190
return new DirectFieldReference(new ExposedField(mappedField, true));
146191
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,7 @@ public Document toDocument(final AggregationOperationContext context) {
170170
private Document toMap(ExposedFields exposedFields, AggregationOperationContext context) {
171171

172172
Document map = new Document();
173-
InheritingExposedFieldsAggregationOperationContext operationContext = new InheritingExposedFieldsAggregationOperationContext(
174-
exposedFields, context, false);
173+
AggregationOperationContext operationContext = context.inheritAndExpose(exposedFields);
175174

176175
Document input;
177176
if (sourceArray instanceof Field field) {
@@ -316,8 +315,7 @@ private Document toLet(ExposedFields exposedFields, AggregationOperationContext
316315
letExpression.put("vars", mappedVars);
317316
if (expression != null) {
318317

319-
InheritingExposedFieldsAggregationOperationContext operationContext = new InheritingExposedFieldsAggregationOperationContext(
320-
exposedFields, context, false);
318+
AggregationOperationContext operationContext = context.inheritAndExpose(exposedFields);
321319
letExpression.put("in", getMappedIn(operationContext));
322320
}
323321

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ void aggregateShouldUseRelaxedMappingByDefault() {
558558
protected <O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName,
559559
Class<O> outputType, AggregationOperationContext context) {
560560

561-
assertThat(context).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class);
561+
assertThat(ReflectionTestUtils.getField(context, "lookupPolicy")).isEqualTo(FieldLookupPolicy.relaxed());
562562
return super.doAggregate(aggregation, collectionName, outputType, context);
563563
}
564564
};

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryOperationsUnitTests.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,21 @@
2525
import org.junit.jupiter.api.extension.ExtendWith;
2626
import org.mockito.Mock;
2727
import org.mockito.junit.jupiter.MockitoExtension;
28+
2829
import org.springframework.data.mapping.context.MappingContext;
2930
import org.springframework.data.mongodb.MongoDatabaseFactory;
3031
import org.springframework.data.mongodb.core.QueryOperations.AggregationDefinition;
3132
import org.springframework.data.mongodb.core.aggregation.Aggregation;
3233
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
33-
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
34+
import org.springframework.data.mongodb.core.aggregation.FieldLookupPolicy;
3435
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
3536
import org.springframework.data.mongodb.core.convert.QueryMapper;
3637
import org.springframework.data.mongodb.core.convert.UpdateMapper;
3738
import org.springframework.data.mongodb.core.mapping.MongoId;
3839
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
3940
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
4041
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
42+
import org.springframework.test.util.ReflectionTestUtils;
4143

4244
/**
4345
* Unit tests for {@link QueryOperations}.
@@ -72,27 +74,33 @@ void beforeEach() {
7274
void createAggregationContextUsesRelaxedOneForUntypedAggregationsWhenNoInputTypeProvided() {
7375

7476
Aggregation aggregation = Aggregation.newAggregation(Aggregation.project("name"));
75-
AggregationDefinition ctx = queryOperations.createAggregation(aggregation, (Class<?>) null);
77+
AggregationDefinition def = queryOperations.createAggregation(aggregation, (Class<?>) null);
78+
TypeBasedAggregationOperationContext ctx = (TypeBasedAggregationOperationContext) def
79+
.getAggregationOperationContext();
7680

77-
assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class);
81+
assertThat(ReflectionTestUtils.getField(ctx, "lookupPolicy")).isEqualTo(FieldLookupPolicy.relaxed());
7882
}
7983

8084
@Test // GH-3542
8185
void createAggregationContextUsesRelaxedOneForTypedAggregationsWhenNoInputTypeProvided() {
8286

8387
Aggregation aggregation = Aggregation.newAggregation(Person.class, Aggregation.project("name"));
84-
AggregationDefinition ctx = queryOperations.createAggregation(aggregation, (Class<?>) null);
88+
AggregationDefinition def = queryOperations.createAggregation(aggregation, Person.class);
89+
TypeBasedAggregationOperationContext ctx = (TypeBasedAggregationOperationContext) def
90+
.getAggregationOperationContext();
8591

86-
assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class);
92+
assertThat(ReflectionTestUtils.getField(ctx, "lookupPolicy")).isEqualTo(FieldLookupPolicy.relaxed());
8793
}
8894

8995
@Test // GH-3542
9096
void createAggregationContextUsesRelaxedOneForUntypedAggregationsWhenInputTypeProvided() {
9197

9298
Aggregation aggregation = Aggregation.newAggregation(Aggregation.project("name"));
93-
AggregationDefinition ctx = queryOperations.createAggregation(aggregation, Person.class);
99+
AggregationDefinition def = queryOperations.createAggregation(aggregation, Person.class);
100+
TypeBasedAggregationOperationContext ctx = (TypeBasedAggregationOperationContext) def
101+
.getAggregationOperationContext();
94102

95-
assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class);
103+
assertThat(ReflectionTestUtils.getField(ctx, "lookupPolicy")).isEqualTo(FieldLookupPolicy.relaxed());
96104
}
97105

98106
@Test // GH-3542

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRendererUnitTests.java

Lines changed: 4 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,15 @@
1515
*/
1616
package org.springframework.data.mongodb.core.aggregation;
1717

18-
import static org.assertj.core.api.Assertions.assertThat;
19-
import static org.mockito.Mockito.eq;
20-
import static org.mockito.Mockito.mock;
21-
import static org.mockito.Mockito.verify;
22-
import static org.mockito.Mockito.when;
23-
import static org.springframework.data.domain.Sort.Direction.DESC;
24-
import static org.springframework.data.mongodb.core.aggregation.Aggregation.project;
25-
import static org.springframework.data.mongodb.core.aggregation.Aggregation.sort;
18+
import static org.mockito.Mockito.*;
19+
import static org.springframework.data.domain.Sort.Direction.*;
20+
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
2621

2722
import java.util.List;
2823

29-
import org.assertj.core.api.InstanceOfAssertFactories;
3024
import org.junit.jupiter.api.Test;
31-
import org.mockito.ArgumentCaptor;
25+
3226
import org.springframework.data.annotation.Id;
33-
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
3427
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
3528
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
3629
import org.springframework.data.mongodb.core.convert.QueryMapper;
@@ -54,80 +47,6 @@ void nonFieldsExposingAggregationOperationContinuesWithSameContextForNextStage()
5447
verify(stage2).toPipelineStages(eq(rootContext));
5548
}
5649

57-
@Test // GH-4443
58-
void fieldsExposingAggregationOperationNotExposingFieldsForcesUseOfDefaultContextForNextStage() {
59-
60-
AggregationOperationContext rootContext = mock(AggregationOperationContext.class);
61-
FieldsExposingAggregationOperation stage1 = mock(FieldsExposingAggregationOperation.class);
62-
ExposedFields stage1fields = mock(ExposedFields.class);
63-
AggregationOperation stage2 = mock(AggregationOperation.class);
64-
65-
when(stage1.getFields()).thenReturn(stage1fields);
66-
when(stage1fields.exposesNoFields()).thenReturn(true);
67-
68-
AggregationOperationRenderer.toDocument(List.of(stage1, stage2), rootContext);
69-
70-
verify(stage1).toPipelineStages(eq(rootContext));
71-
verify(stage2).toPipelineStages(eq(AggregationOperationRenderer.DEFAULT_CONTEXT));
72-
}
73-
74-
@Test // GH-4443
75-
void fieldsExposingAggregationOperationForcesNewContextForNextStage() {
76-
77-
AggregationOperationContext rootContext = mock(AggregationOperationContext.class);
78-
FieldsExposingAggregationOperation stage1 = mock(FieldsExposingAggregationOperation.class);
79-
ExposedFields stage1fields = mock(ExposedFields.class);
80-
AggregationOperation stage2 = mock(AggregationOperation.class);
81-
82-
when(stage1.getFields()).thenReturn(stage1fields);
83-
when(stage1fields.exposesNoFields()).thenReturn(false);
84-
85-
ArgumentCaptor<AggregationOperationContext> captor = ArgumentCaptor.forClass(AggregationOperationContext.class);
86-
87-
AggregationOperationRenderer.toDocument(List.of(stage1, stage2), rootContext);
88-
89-
verify(stage1).toPipelineStages(eq(rootContext));
90-
verify(stage2).toPipelineStages(captor.capture());
91-
92-
assertThat(captor.getValue()).isInstanceOf(ExposedFieldsAggregationOperationContext.class)
93-
.isNotInstanceOf(InheritingExposedFieldsAggregationOperationContext.class);
94-
}
95-
96-
@Test // GH-4443
97-
void inheritingFieldsExposingAggregationOperationForcesNewContextForNextStageKeepingReferenceToPreviousContext() {
98-
99-
AggregationOperationContext rootContext = mock(AggregationOperationContext.class);
100-
InheritsFieldsAggregationOperation stage1 = mock(InheritsFieldsAggregationOperation.class);
101-
InheritsFieldsAggregationOperation stage2 = mock(InheritsFieldsAggregationOperation.class);
102-
InheritsFieldsAggregationOperation stage3 = mock(InheritsFieldsAggregationOperation.class);
103-
104-
ExposedFields exposedFields = mock(ExposedFields.class);
105-
when(exposedFields.exposesNoFields()).thenReturn(false);
106-
when(stage1.getFields()).thenReturn(exposedFields);
107-
when(stage2.getFields()).thenReturn(exposedFields);
108-
when(stage3.getFields()).thenReturn(exposedFields);
109-
110-
ArgumentCaptor<AggregationOperationContext> captor = ArgumentCaptor.forClass(AggregationOperationContext.class);
111-
112-
AggregationOperationRenderer.toDocument(List.of(stage1, stage2, stage3), rootContext);
113-
114-
verify(stage1).toPipelineStages(captor.capture());
115-
verify(stage2).toPipelineStages(captor.capture());
116-
verify(stage3).toPipelineStages(captor.capture());
117-
118-
assertThat(captor.getAllValues().get(0)).isEqualTo(rootContext);
119-
120-
assertThat(captor.getAllValues().get(1))
121-
.asInstanceOf(InstanceOfAssertFactories.type(InheritingExposedFieldsAggregationOperationContext.class))
122-
.extracting("previousContext").isSameAs(captor.getAllValues().get(0));
123-
124-
assertThat(captor.getAllValues().get(2))
125-
.asInstanceOf(InstanceOfAssertFactories.type(InheritingExposedFieldsAggregationOperationContext.class))
126-
.extracting("previousContext").isSameAs(captor.getAllValues().get(1));
127-
}
128-
129-
130-
13150
record TestRecord(@Id String field1, String field2, LayerOne layerOne) {
13251
record LayerOne(List<LayerTwo> layerTwo) {
13352
}

0 commit comments

Comments
 (0)
Please sign in to comment.