1515 */
1616package org .springframework .data .mongodb .core .aggregation ;
1717
18- import java .util .List ;
18+ import java .util .function . Supplier ;
1919
2020import org .bson .Document ;
2121import org .springframework .data .mongodb .core .aggregation .ExposedFields .ExposedField ;
2222import org .springframework .data .mongodb .core .aggregation .FieldsExposingAggregationOperation .InheritsFieldsAggregationOperation ;
23+ import org .springframework .data .mongodb .core .aggregation .VariableOperators .Let ;
24+ import org .springframework .data .mongodb .core .aggregation .VariableOperators .Let .ExpressionVariable ;
2325import org .springframework .lang .Nullable ;
2426import org .springframework .util .Assert ;
2527
2628/**
27- * Encapsulates the aggregation framework {@code $lookup}-operation. We recommend to use the static factory method
28- * {@link Aggregation#lookup(String, String, String, String )} instead of creating instances of this class directly.
29+ * Encapsulates the aggregation framework {@code $lookup}-operation. We recommend to use the builder provided via
30+ * {@link #newLookup( )} instead of creating instances of this class directly.
2931 *
3032 * @author Alessio Fachechi
3133 * @author Christoph Strobl
3739 */
3840public class LookupOperation implements FieldsExposingAggregationOperation , InheritsFieldsAggregationOperation {
3941
40- private final Field from ;
42+ private final String from ;
43+
44+ @ Nullable //
4145 private final Field localField ;
46+
47+ @ Nullable //
4248 private final Field foreignField ;
43- private final ExposedField as ;
4449
45- @ Nullable
50+ @ Nullable //
4651 private final Let let ;
47- @ Nullable
52+
53+ @ Nullable //
4854 private final AggregationPipeline pipeline ;
4955
56+ private final ExposedField as ;
57+
5058 /**
5159 * Creates a new {@link LookupOperation} for the given {@link Field}s.
5260 *
@@ -56,13 +64,47 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
5664 * @param as must not be {@literal null}.
5765 */
5866 public LookupOperation (Field from , Field localField , Field foreignField , Field as ) {
59- this (from , localField , foreignField , as , null , null );
67+ this (((Supplier <String >) () -> {
68+
69+ Assert .notNull (from , "From must not be null" );
70+ return from .getTarget ();
71+ }).get (), localField , foreignField , null , null , as );
6072 }
6173
62- public LookupOperation (Field from , Field localField , Field foreignField , Field as , @ Nullable Let let , @ Nullable AggregationPipeline pipeline ) {
74+ /**
75+ * Creates a new {@link LookupOperation} for the given combination of {@link Field}s and {@link AggregationPipeline
76+ * pipeline}.
77+ *
78+ * @param from must not be {@literal null}.
79+ * @param let must not be {@literal null}.
80+ * @param as must not be {@literal null}.
81+ * @since 4.1
82+ */
83+ public LookupOperation (String from , @ Nullable Let let , AggregationPipeline pipeline , Field as ) {
84+ this (from , null , null , let , pipeline , as );
85+ }
86+
87+ /**
88+ * Creates a new {@link LookupOperation} for the given combination of {@link Field}s and {@link AggregationPipeline
89+ * pipeline}.
90+ *
91+ * @param from must not be {@literal null}.
92+ * @param localField can be {@literal null} if {@literal pipeline} is present.
93+ * @param foreignField can be {@literal null} if {@literal pipeline} is present.
94+ * @param let can be {@literal null} if {@literal localField} and {@literal foreignField} are present.
95+ * @param as must not be {@literal null}.
96+ * @since 4.1
97+ */
98+ public LookupOperation (String from , @ Nullable Field localField , @ Nullable Field foreignField , @ Nullable Let let ,
99+ @ Nullable AggregationPipeline pipeline , Field as ) {
100+
63101 Assert .notNull (from , "From must not be null" );
64- Assert .notNull (localField , "LocalField must not be null" );
65- Assert .notNull (foreignField , "ForeignField must not be null" );
102+ if (pipeline == null ) {
103+ Assert .notNull (localField , "LocalField must not be null" );
104+ Assert .notNull (foreignField , "ForeignField must not be null" );
105+ } else if (localField == null && foreignField == null ) {
106+ Assert .notNull (pipeline , "Pipeline must not be null" );
107+ }
66108 Assert .notNull (as , "As must not be null" );
67109
68110 this .from = from ;
@@ -83,19 +125,22 @@ public Document toDocument(AggregationOperationContext context) {
83125
84126 Document lookupObject = new Document ();
85127
86- lookupObject .append ("from" , from .getTarget ());
87- lookupObject .append ("localField" , localField .getTarget ());
88- lookupObject .append ("foreignField" , foreignField .getTarget ());
89- lookupObject .append ("as" , as .getTarget ());
90-
128+ lookupObject .append ("from" , from );
129+ if (localField != null ) {
130+ lookupObject .append ("localField" , localField .getTarget ());
131+ }
132+ if (foreignField != null ) {
133+ lookupObject .append ("foreignField" , foreignField .getTarget ());
134+ }
91135 if (let != null ) {
92- lookupObject .append ("let" , let .toDocument (context ));
136+ lookupObject .append ("let" , let .toDocument (context ). get ( "$let" , Document . class ). get ( "vars" ) );
93137 }
94-
95138 if (pipeline != null ) {
96139 lookupObject .append ("pipeline" , pipeline .toDocuments (context ));
97140 }
98141
142+ lookupObject .append ("as" , as .getTarget ());
143+
99144 return new Document (getOperator (), lookupObject );
100145 }
101146
@@ -122,7 +167,7 @@ public static interface FromBuilder {
122167 LocalFieldBuilder from (String name );
123168 }
124169
125- public static interface LocalFieldBuilder {
170+ public static interface LocalFieldBuilder extends PipelineBuilder {
126171
127172 /**
128173 * @param name the field from the documents input to the {@code $lookup} stage, must not be {@literal null} or
@@ -141,7 +186,67 @@ public static interface ForeignFieldBuilder {
141186 AsBuilder foreignField (String name );
142187 }
143188
144- public static interface AsBuilder {
189+ /**
190+ * @since 4.1
191+ * @author Christoph Strobl
192+ */
193+ public interface LetBuilder {
194+
195+ /**
196+ * Specifies {@link Let#getVariableNames() variables) that can be used in the
197+ * {@link PipelineBuilder#pipeline(AggregationOperation...) pipeline stages}.
198+ *
199+ * @param let must not be {@literal null}.
200+ * @return never {@literal null}.
201+ * @see PipelineBuilder
202+ */
203+ PipelineBuilder let (Let let );
204+
205+ /**
206+ * Specifies {@link Let#getVariableNames() variables) that can be used in the
207+ * {@link PipelineBuilder#pipeline(AggregationOperation...) pipeline stages}.
208+ *
209+ * @param variables must not be {@literal null}.
210+ * @return never {@literal null}.
211+ * @see PipelineBuilder
212+ */
213+ default PipelineBuilder let (ExpressionVariable ... variables ) {
214+ return let (Let .just (variables ));
215+ }
216+ }
217+
218+ /**
219+ * @since 4.1
220+ * @author Christoph Strobl
221+ */
222+ public interface PipelineBuilder extends LetBuilder {
223+
224+ /**
225+ * Specifies the {@link AggregationPipeline pipeline} that determines the resulting documents.
226+ *
227+ * @param pipeline must not be {@literal null}.
228+ * @return never {@literal null}.
229+ */
230+ AsBuilder pipeline (AggregationPipeline pipeline );
231+
232+ /**
233+ * Specifies the {@link AggregationPipeline#getOperations() stages} that determine the resulting documents.
234+ *
235+ * @param stages must not be {@literal null} can be empty.
236+ * @return never {@literal null}.
237+ */
238+ default AsBuilder pipeline (AggregationOperation ... stages ) {
239+ return pipeline (AggregationPipeline .of (stages ));
240+ }
241+
242+ /**
243+ * @param name the name of the new array field to add to the input documents, must not be {@literal null} or empty.
244+ * @return new instance of {@link LookupOperation}.
245+ */
246+ LookupOperation as (String name );
247+ }
248+
249+ public static interface AsBuilder extends PipelineBuilder {
145250
146251 /**
147252 * @param name the name of the new array field to add to the input documents, must not be {@literal null} or empty.
@@ -159,10 +264,12 @@ public static interface AsBuilder {
159264 public static final class LookupOperationBuilder
160265 implements FromBuilder , LocalFieldBuilder , ForeignFieldBuilder , AsBuilder {
161266
162- private @ Nullable Field from ;
267+ private @ Nullable String from ;
163268 private @ Nullable Field localField ;
164269 private @ Nullable Field foreignField ;
165270 private @ Nullable ExposedField as ;
271+ private @ Nullable Let let ;
272+ private @ Nullable AggregationPipeline pipeline ;
166273
167274 /**
168275 * Creates new builder for {@link LookupOperation}.
@@ -177,18 +284,10 @@ public static FromBuilder newBuilder() {
177284 public LocalFieldBuilder from (String name ) {
178285
179286 Assert .hasText (name , "'From' must not be null or empty" );
180- from = Fields . field ( name ) ;
287+ from = name ;
181288 return this ;
182289 }
183290
184- @ Override
185- public LookupOperation as (String name ) {
186-
187- Assert .hasText (name , "'As' must not be null or empty" );
188- as = new ExposedField (Fields .field (name ), true );
189- return new LookupOperation (from , localField , foreignField , as );
190- }
191-
192291 @ Override
193292 public AsBuilder foreignField (String name ) {
194293
@@ -204,50 +303,29 @@ public ForeignFieldBuilder localField(String name) {
204303 localField = Fields .field (name );
205304 return this ;
206305 }
207- }
208-
209- public static class Let implements AggregationExpression {
210-
211- private final List <ExpressionVariable > vars ;
212-
213- public Let (List <ExpressionVariable > vars ) {
214- Assert .notEmpty (vars , "'let' must not be null or empty" );
215- this .vars = vars ;
216- }
217306
218307 @ Override
219- public Document toDocument (AggregationOperationContext context ) {
220- return toLet ();
221- }
222-
223- private Document toLet () {
224- Document mappedVars = new Document ();
308+ public PipelineBuilder let (Let let ) {
225309
226- for (ExpressionVariable var : this .vars ) {
227- mappedVars .putAll (getMappedVariable (var ));
228- }
229-
230- return mappedVars ;
310+ Assert .notNull (let , "Let must not be null" );
311+ this .let = let ;
312+ return this ;
231313 }
232314
233- private Document getMappedVariable (ExpressionVariable var ) {
234- return new Document (var .variableName , prefixDollarSign (var .expression ));
235- }
315+ @ Override
316+ public AsBuilder pipeline (AggregationPipeline pipeline ) {
236317
237- private String prefixDollarSign (String expression ) {
238- return "$" + expression ;
318+ Assert .notNull (pipeline , "Pipeline must not be null" );
319+ this .pipeline = pipeline ;
320+ return this ;
239321 }
240322
241- public static class ExpressionVariable {
242-
243- private final String variableName ;
244-
245- private final String expression ;
323+ @ Override
324+ public LookupOperation as (String name ) {
246325
247- public ExpressionVariable (String variableName , String expression ) {
248- this .variableName = variableName ;
249- this .expression = expression ;
250- }
326+ Assert .hasText (name , "'As' must not be null or empty" );
327+ as = new ExposedField (Fields .field (name ), true );
328+ return new LookupOperation (from , localField , foreignField , let , pipeline , as );
251329 }
252330 }
253331}
0 commit comments