Skip to content

Commit 86dbd95

Browse files
DATAMONGO-1551 - Polishing.
Add startWith overload allowing to mix expressions, removed white spaces, updated doc. Original Pull Request: spring-projects#424
1 parent 3d0750a commit 86dbd95

File tree

4 files changed

+121
-38
lines changed

4 files changed

+121
-38
lines changed

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,12 @@ public static GroupOperation group(Fields fields) {
318318
}
319319

320320
/**
321-
* Creates a new {@link GraphLookupOperation.FromBuilder} to construct a {@link GraphLookupOperation} given
322-
* {@literal fromCollection}.
321+
* Creates a new {@link GraphLookupOperation.GraphLookupOperationFromBuilder} to construct a
322+
* {@link GraphLookupOperation} given {@literal fromCollection}.
323323
*
324324
* @param fromCollection must not be {@literal null} or empty.
325325
* @return
326+
* @since 1.10
326327
*/
327328
public static StartWithBuilder graphLookup(String fromCollection) {
328329
return GraphLookupOperation.builder().from(fromCollection);

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

+80-34
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,35 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.Arrays;
20+
import java.util.HashSet;
2021
import java.util.List;
22+
import java.util.Set;
2123

24+
import org.bson.Document;
2225
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
2326
import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation;
2427
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
2528
import org.springframework.util.Assert;
26-
27-
import org.bson.Document;
29+
import org.springframework.util.ClassUtils;
2830

2931
/**
30-
* Encapsulates the aggregation framework {@code $graphLookup}-operation.
31-
* <p>
32+
* Encapsulates the aggregation framework {@code $graphLookup}-operation. <br />
3233
* Performs a recursive search on a collection, with options for restricting the search by recursion depth and query
33-
* filter.
34-
* <p>
34+
* filter. <br />
3535
* We recommend to use the static factory method {@link Aggregation#graphLookup(String)} instead of creating instances
3636
* of this class directly.
3737
*
38-
* @see http://docs.mongodb.org/manual/reference/aggregation/graphLookup/
38+
* @see <a href=
39+
* "http://docs.mongodb.org/manual/reference/aggregation/graphLookup/">http://docs.mongodb.org/manual/reference/aggregation/graphLookup/</a>
3940
* @author Mark Paluch
41+
* @author Christoph Strobl
4042
* @since 1.10
4143
*/
4244
public class GraphLookupOperation implements InheritsFieldsAggregationOperation {
4345

46+
private static final Set<Class<?>> ALLOWED_START_TYPES = new HashSet<Class<?>>(
47+
Arrays.<Class<?>> asList(AggregationExpression.class, String.class, Field.class, Document.class));
48+
4449
private final String from;
4550
private final List<Object> startWith;
4651
private final Field connectFrom;
@@ -65,15 +70,15 @@ private GraphLookupOperation(String from, List<Object> startWith, Field connectF
6570

6671
/**
6772
* Creates a new {@link FromBuilder} to build {@link GraphLookupOperation}.
68-
*
73+
*
6974
* @return a new {@link FromBuilder}.
7075
*/
7176
public static FromBuilder builder() {
7277
return new GraphLookupOperationFromBuilder();
7378
}
7479

7580
/* (non-Javadoc)
76-
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
81+
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
7782
*/
7883
@Override
7984
public Document toDocument(AggregationOperationContext context) {
@@ -82,24 +87,20 @@ public Document toDocument(AggregationOperationContext context) {
8287

8388
graphLookup.put("from", from);
8489

85-
List<Object> list = new ArrayList<>(startWith.size());
90+
List<Object> mappedStartWith = new ArrayList<Object>(startWith.size());
8691

8792
for (Object startWithElement : startWith) {
8893

8994
if (startWithElement instanceof AggregationExpression) {
90-
list.add(((AggregationExpression) startWithElement).toDocument(context));
91-
}
92-
93-
if (startWithElement instanceof Field) {
94-
list.add(context.getReference((Field) startWithElement).toString());
95+
mappedStartWith.add(((AggregationExpression) startWithElement).toDocument(context));
96+
} else if (startWithElement instanceof Field) {
97+
mappedStartWith.add(context.getReference((Field) startWithElement).toString());
98+
} else {
99+
mappedStartWith.add(startWithElement);
95100
}
96101
}
97102

98-
if (list.size() == 1) {
99-
graphLookup.put("startWith", list.get(0));
100-
} else {
101-
graphLookup.put("startWith", list);
102-
}
103+
graphLookup.put("startWith", mappedStartWith.size() == 1 ? mappedStartWith.iterator().next() : mappedStartWith);
103104

104105
graphLookup.put("connectFromField", connectFrom.getName());
105106
graphLookup.put("connectToField", connectTo.getName());
@@ -145,6 +146,7 @@ public interface FromBuilder {
145146

146147
/**
147148
* @author Mark Paluch
149+
* @author Christoph Strobl
148150
*/
149151
public interface StartWithBuilder {
150152

@@ -163,6 +165,16 @@ public interface StartWithBuilder {
163165
* @return
164166
*/
165167
ConnectFromBuilder startWith(AggregationExpression... expressions);
168+
169+
/**
170+
* Set the startWith as either {@literal fieldReferences}, {@link Fields}, {@link Document} or
171+
* {@link AggregationExpression} to apply the {@code $graphLookup} to.
172+
*
173+
* @param expressions must not be {@literal null}.
174+
* @return
175+
* @throws IllegalArgumentException
176+
*/
177+
ConnectFromBuilder startWith(Object... expressions);
166178
}
167179

168180
/**
@@ -196,7 +208,7 @@ public interface ConnectToBuilder {
196208
/**
197209
* Builder to build the initial {@link GraphLookupOperationBuilder} that configures the initial mandatory set of
198210
* {@link GraphLookupOperation} properties.
199-
*
211+
*
200212
* @author Mark Paluch
201213
*/
202214
static final class GraphLookupOperationFromBuilder
@@ -215,7 +227,6 @@ public StartWithBuilder from(String collectionName) {
215227
Assert.hasText(collectionName, "CollectionName must not be null or empty!");
216228

217229
this.from = collectionName;
218-
219230
return this;
220231
}
221232

@@ -235,7 +246,6 @@ public ConnectFromBuilder startWith(String... fieldReferences) {
235246
}
236247

237248
this.startWith = fields;
238-
239249
return this;
240250
}
241251

@@ -249,10 +259,50 @@ public ConnectFromBuilder startWith(AggregationExpression... expressions) {
249259
Assert.noNullElements(expressions, "AggregationExpressions must not contain null elements!");
250260

251261
this.startWith = Arrays.asList(expressions);
262+
return this;
263+
}
264+
265+
@Override
266+
public ConnectFromBuilder startWith(Object... expressions) {
252267

268+
Assert.notNull(expressions, "Expressions must not be null!");
269+
Assert.noNullElements(expressions, "Expressions must not contain null elements!");
270+
271+
this.startWith = verifyAndPotentiallyTransformStartsWithTypes(expressions);
253272
return this;
254273
}
255274

275+
private List<Object> verifyAndPotentiallyTransformStartsWithTypes(Object... expressions) {
276+
277+
List<Object> expressionsToUse = new ArrayList<Object>(expressions.length);
278+
279+
for (Object expression : expressions) {
280+
281+
assertStartWithType(expression);
282+
283+
if (expression instanceof String) {
284+
expressionsToUse.add(Fields.field((String) expression));
285+
} else {
286+
expressionsToUse.add(expression);
287+
}
288+
289+
}
290+
return expressionsToUse;
291+
}
292+
293+
private void assertStartWithType(Object expression) {
294+
295+
for (Class<?> type : ALLOWED_START_TYPES) {
296+
297+
if (ClassUtils.isAssignable(type, expression.getClass())) {
298+
return;
299+
}
300+
}
301+
302+
throw new IllegalArgumentException(
303+
String.format("Expression must be any of %s but was %s", ALLOWED_START_TYPES, expression.getClass()));
304+
}
305+
256306
/* (non-Javadoc)
257307
* @see org.springframework.data.mongodb.core.aggregation.GraphLookupOperation.ConnectFromBuilder#connectFrom(java.lang.String)
258308
*/
@@ -262,7 +312,6 @@ public ConnectToBuilder connectFrom(String fieldName) {
262312
Assert.hasText(fieldName, "ConnectFrom must not be null or empty!");
263313

264314
this.connectFrom = fieldName;
265-
266315
return this;
267316
}
268317

@@ -301,8 +350,8 @@ protected GraphLookupOperationBuilder(String from, List<? extends Object> startW
301350
}
302351

303352
/**
304-
* Limit the number of recursions.
305-
*
353+
* Optionally limit the number of recursions.
354+
*
306355
* @param numberOfRecursions must be greater or equal to zero.
307356
* @return
308357
*/
@@ -311,13 +360,12 @@ public GraphLookupOperationBuilder maxDepth(long numberOfRecursions) {
311360
Assert.isTrue(numberOfRecursions >= 0, "Max depth must be >= 0!");
312361

313362
this.maxDepth = numberOfRecursions;
314-
315363
return this;
316364
}
317365

318366
/**
319-
* Add a depth field {@literal fieldName} to each traversed document in the search path.
320-
*
367+
* Optionally add a depth field {@literal fieldName} to each traversed document in the search path.
368+
*
321369
* @param fieldName must not be {@literal null} or empty.
322370
* @return
323371
*/
@@ -326,13 +374,12 @@ public GraphLookupOperationBuilder depthField(String fieldName) {
326374
Assert.hasText(fieldName, "Depth field name must not be null or empty!");
327375

328376
this.depthField = Fields.field(fieldName);
329-
330377
return this;
331378
}
332379

333380
/**
334-
* Add a query specifying conditions to the recursive search.
335-
*
381+
* Optionally add a query specifying conditions to the recursive search.
382+
*
336383
* @param criteriaDefinition must not be {@literal null}.
337384
* @return
338385
*/
@@ -341,14 +388,13 @@ public GraphLookupOperationBuilder restrict(CriteriaDefinition criteriaDefinitio
341388
Assert.notNull(criteriaDefinition, "CriteriaDefinition must not be null!");
342389

343390
this.restrictSearchWithMatch = criteriaDefinition;
344-
345391
return this;
346392
}
347393

348394
/**
349395
* Set the name of the array field added to each output document and return the final {@link GraphLookupOperation}.
350396
* Contains the documents traversed in the {@literal $graphLookup} stage to reach the document.
351-
*
397+
*
352398
* @param fieldName must not be {@literal null} or empty.
353399
* @return the final {@link GraphLookupOperation}.
354400
*/

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

+37-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.bson.Document;
2323
import org.junit.Test;
24+
import org.springframework.data.mongodb.core.Person;
2425
import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Literal;
2526
import org.springframework.data.mongodb.core.query.Criteria;
2627

@@ -30,8 +31,9 @@
3031

3132
/**
3233
* Unit tests for {@link GraphLookupOperation}.
33-
*
34+
*
3435
* @author Mark Paluch
36+
* @author Christoph Strobl
3537
*/
3638
public class GraphLookupOperationUnitTests {
3739

@@ -102,6 +104,40 @@ public void shouldRenderArrayOfStartsWithCorrectly() {
102104
+ "connectFromField: \"reportsTo\", connectToField: \"name\", as: \"reportingHierarchy\" } }")));
103105
}
104106

107+
/**
108+
* @see DATAMONGO-1551
109+
*/
110+
@Test
111+
public void shouldRenderMixedArrayOfStartsWithCorrectly() {
112+
113+
GraphLookupOperation graphLookupOperation = GraphLookupOperation.builder() //
114+
.from("employees") //
115+
.startWith("reportsTo", Literal.asLiteral("$boss")) //
116+
.connectFrom("reportsTo") //
117+
.connectTo("name") //
118+
.as("reportingHierarchy");
119+
120+
Document document = graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT);
121+
122+
assertThat(document,
123+
is(Document.parse("{ $graphLookup : { from: \"employees\", startWith: [\"$reportsTo\", { $literal: \"$boss\"}], "
124+
+ "connectFromField: \"reportsTo\", connectToField: \"name\", as: \"reportingHierarchy\" } }")));
125+
}
126+
127+
/**
128+
* @see DATAMONGO-1551
129+
*/
130+
@Test(expected = IllegalArgumentException.class)
131+
public void shouldRejectUnknownTypeInMixedArrayOfStartsWithCorrectly() {
132+
133+
GraphLookupOperation graphLookupOperation = GraphLookupOperation.builder() //
134+
.from("employees") //
135+
.startWith("reportsTo", new Person()) //
136+
.connectFrom("reportsTo") //
137+
.connectTo("name") //
138+
.as("reportingHierarchy");
139+
}
140+
105141
/**
106142
* @see DATAMONGO-1551
107143
*/

src/main/asciidoc/reference/mongodb.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1676,7 +1676,7 @@ At the time of this writing we provide support for the following Aggregation Ope
16761676
[cols="2*"]
16771677
|===
16781678
| Pipeline Aggregation Operators
1679-
| count, geoNear, group, limit, lookup, match, project, replaceRoot, skip, sort, unwind
1679+
| count, geoNear, graphLookup, group, limit, lookup, match, project, replaceRoot, skip, sort, unwind
16801680

16811681
| Set Aggregation Operators
16821682
| setEquals, setIntersection, setUnion, setDifference, setIsSubset, anyElementTrue, allElementsTrue

0 commit comments

Comments
 (0)