diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AdditionalFieldsExposingAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AdditionalFieldsExposingAggregationOperation.java
new file mode 100644
index 0000000000..52b851f534
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AdditionalFieldsExposingAggregationOperation.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.core.aggregation;
+
+/**
+ * {@link AggregationOperation} that exposes additional {@link ExposedFields} that can be used for later
+ * aggregation pipeline {@code AggregationOperation}s, e.g. lookup operation produces a field which has to be added to
+ * the current ones.
+ *
+ * @author Alessio Fachechi
+ */
+public interface AdditionalFieldsExposingAggregationOperation extends FieldsExposingAggregationOperation {
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
index a57db53034..f6f42b6ff2 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
@@ -37,10 +37,11 @@
/**
* An {@code Aggregation} is a representation of a list of aggregation steps to be performed by the MongoDB Aggregation
* Framework.
- *
+ *
* @author Tobias Trelle
* @author Thomas Darimont
* @author Oliver Gierke
+ * @author Alessio Fachechi
* @since 1.3
*/
public class Aggregation {
@@ -65,7 +66,7 @@ public class Aggregation {
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
- *
+ *
* @param operations must not be {@literal null} or empty.
*/
public static Aggregation newAggregation(List extends AggregationOperation> operations) {
@@ -74,7 +75,7 @@ public static Aggregation newAggregation(List extends AggregationOperation> op
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
- *
+ *
* @param operations must not be {@literal null} or empty.
*/
public static Aggregation newAggregation(AggregationOperation... operations) {
@@ -84,7 +85,7 @@ public static Aggregation newAggregation(AggregationOperation... operations) {
/**
* Returns a copy of this {@link Aggregation} with the given {@link AggregationOptions} set. Note that options are
* supported in MongoDB version 2.6+.
- *
+ *
* @param options must not be {@literal null}.
* @return
* @since 1.6
@@ -97,7 +98,7 @@ public Aggregation withOptions(AggregationOptions options) {
/**
* Creates a new {@link TypedAggregation} for the given type and {@link AggregationOperation}s.
- *
+ *
* @param type must not be {@literal null}.
* @param operations must not be {@literal null} or empty.
*/
@@ -107,7 +108,7 @@ public static TypedAggregation newAggregation(Class type, List exten
/**
* Creates a new {@link TypedAggregation} for the given type and {@link AggregationOperation}s.
- *
+ *
* @param type must not be {@literal null}.
* @param operations must not be {@literal null} or empty.
*/
@@ -117,7 +118,7 @@ public static TypedAggregation newAggregation(Class type, AggregationO
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
- *
+ *
* @param aggregationOperations must not be {@literal null} or empty.
*/
protected Aggregation(AggregationOperation... aggregationOperations) {
@@ -137,7 +138,7 @@ protected static List asAggregationList(AggregationOperati
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
- *
+ *
* @param aggregationOperations must not be {@literal null} or empty.
*/
protected Aggregation(List aggregationOperations) {
@@ -146,7 +147,7 @@ protected Aggregation(List aggregationOperations) {
/**
* Creates a new {@link Aggregation} from the given {@link AggregationOperation}s.
- *
+ *
* @param aggregationOperations must not be {@literal null} or empty.
* @param options must not be {@literal null} or empty.
*/
@@ -162,7 +163,7 @@ protected Aggregation(List aggregationOperations, Aggregat
/**
* A pointer to the previous {@link AggregationOperation}.
- *
+ *
* @return
*/
public static String previousOperation() {
@@ -171,7 +172,7 @@ public static String previousOperation() {
/**
* Creates a new {@link ProjectionOperation} including the given fields.
- *
+ *
* @param fields must not be {@literal null}.
* @return
*/
@@ -181,7 +182,7 @@ public static ProjectionOperation project(String... fields) {
/**
* Creates a new {@link ProjectionOperation} includeing the given {@link Fields}.
- *
+ *
* @param fields must not be {@literal null}.
* @return
*/
@@ -191,7 +192,7 @@ public static ProjectionOperation project(Fields fields) {
/**
* Factory method to create a new {@link UnwindOperation} for the field with the given name.
- *
+ *
* @param fieldName must not be {@literal null} or empty.
* @return
*/
@@ -201,7 +202,7 @@ public static UnwindOperation unwind(String field) {
/**
* Creates a new {@link GroupOperation} for the given fields.
- *
+ *
* @param fields must not be {@literal null}.
* @return
*/
@@ -211,7 +212,7 @@ public static GroupOperation group(String... fields) {
/**
* Creates a new {@link GroupOperation} for the given {@link Fields}.
- *
+ *
* @param fields must not be {@literal null}.
* @return
*/
@@ -221,7 +222,7 @@ public static GroupOperation group(Fields fields) {
/**
* Factory method to create a new {@link SortOperation} for the given {@link Sort}.
- *
+ *
* @param sort must not be {@literal null}.
* @return
*/
@@ -231,7 +232,7 @@ public static SortOperation sort(Sort sort) {
/**
* Factory method to create a new {@link SortOperation} for the given sort {@link Direction} and {@code fields}.
- *
+ *
* @param direction must not be {@literal null}.
* @param fields must not be {@literal null}.
* @return
@@ -242,7 +243,7 @@ public static SortOperation sort(Direction direction, String... fields) {
/**
* Creates a new {@link SkipOperation} skipping the given number of elements.
- *
+ *
* @param elementsToSkip must not be less than zero.
* @return
*/
@@ -252,7 +253,7 @@ public static SkipOperation skip(int elementsToSkip) {
/**
* Creates a new {@link LimitOperation} limiting the result to the given number of elements.
- *
+ *
* @param maxElements must not be less than zero.
* @return
*/
@@ -262,7 +263,7 @@ public static LimitOperation limit(long maxElements) {
/**
* Creates a new {@link MatchOperation} using the given {@link Criteria}.
- *
+ *
* @param criteria must not be {@literal null}.
* @return
*/
@@ -270,12 +271,32 @@ public static MatchOperation match(Criteria criteria) {
return new MatchOperation(criteria);
}
+ /**
+ * Creates a new {@link LookupOperation} for the given fields.
+ *
+ * @param fields must not be {@literal null}.
+ * @return
+ */
+ public static LookupOperation lookup(String from, String localField, String foreignField, String as) {
+ return lookup(field(from), field(localField), field(foreignField), field(as));
+ }
+
+ /**
+ * Creates a new {@link LookupOperation} for the given {@link Fields}.
+ *
+ * @param fields must not be {@literal null}.
+ * @return
+ */
+ public static LookupOperation lookup(Field from, Field localField, Field foreignField, Field as) {
+ return new LookupOperation(from, localField, foreignField, as);
+ }
+
/**
* Creates a new {@link Fields} instance for the given field names.
- *
- * @see Fields#fields(String...)
+ *
* @param fields must not be {@literal null}.
* @return
+ * @see Fields#fields(String...)
*/
public static Fields fields(String... fields) {
return Fields.fields(fields);
@@ -283,7 +304,7 @@ public static Fields fields(String... fields) {
/**
* Creates a new {@link Fields} instance from the given field name and target reference.
- *
+ *
* @param name must not be {@literal null} or empty.
* @param target must not be {@literal null} or empty.
* @return
@@ -295,7 +316,7 @@ public static Fields bind(String name, String target) {
/**
* Creates a new {@link GeoNearOperation} instance from the given {@link NearQuery} and the{@code distanceField}. The
* {@code distanceField} defines output field that contains the calculated distance.
- *
+ *
* @param query must not be {@literal null}.
* @param distanceField must not be {@literal null} or empty.
* @return
@@ -307,7 +328,7 @@ public static GeoNearOperation geoNear(NearQuery query, String distanceField) {
/**
* Returns a new {@link AggregationOptions.Builder}.
- *
+ *
* @return
* @since 1.6
*/
@@ -317,7 +338,7 @@ public static AggregationOptions.Builder newAggregationOptions() {
/**
* Converts this {@link Aggregation} specification to a {@link DBObject}.
- *
+ *
* @param inputCollectionName the name of the input collection
* @return the {@code DBObject} representing this aggregation
*/
@@ -331,8 +352,11 @@ public DBObject toDbObject(String inputCollectionName, AggregationOperationConte
operationDocuments.add(operation.toDBObject(context));
if (operation instanceof FieldsExposingAggregationOperation) {
+ boolean additional = operation instanceof AdditionalFieldsExposingAggregationOperation ? true : false;
+
FieldsExposingAggregationOperation exposedFieldsOperation = (FieldsExposingAggregationOperation) operation;
- context = new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), rootContext);
+ context = new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), rootContext,
+ additional);
}
}
@@ -356,7 +380,7 @@ public String toString() {
/**
* Simple {@link AggregationOperationContext} that just returns {@link FieldReference}s as is.
- *
+ *
* @author Oliver Gierke
*/
private static class NoOpAggregationOperationContext implements AggregationOperationContext {
@@ -391,7 +415,7 @@ public FieldReference getReference(String name) {
/**
* Describes the system variables available in MongoDB aggregation framework pipeline expressions.
- *
+ *
* @author Thomas Darimont
* @see http://docs.mongodb.org/manual/reference/aggregation-variables
*/
@@ -404,7 +428,7 @@ enum SystemVariable {
/**
* Return {@literal true} if the given {@code fieldRef} denotes a well-known system variable, {@literal false}
* otherwise.
- *
+ *
* @param fieldRef may be {@literal null}.
* @return
*/
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java
index 0e3a05d3ce..3e23db9272 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java
@@ -17,37 +17,57 @@
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
+import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField;
import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
import com.mongodb.DBObject;
/**
* {@link AggregationOperationContext} that combines the available field references from a given
* {@code AggregationOperationContext} and an {@link FieldsExposingAggregationOperation}.
- *
+ *
* @author Thomas Darimont
* @author Oliver Gierke
+ * @author Alessio Fachechi
* @since 1.4
*/
class ExposedFieldsAggregationOperationContext implements AggregationOperationContext {
private final ExposedFields exposedFields;
private final AggregationOperationContext rootContext;
+ private final boolean additional;
+
+ /**
+ * Creates a new {@link ExposedFieldsAggregationOperationContext} from the given {@link ExposedFields}. Uses the given
+ * {@link AggregationOperationContext} to perform a mapping to mongo types if necessary.
+ *
+ * @param exposedFields must not be {@literal null}.
+ * @param rootContext must not be {@literal null}.
+ */
+ public ExposedFieldsAggregationOperationContext(ExposedFields exposedFields,
+ AggregationOperationContext rootContext) {
+ this(exposedFields, rootContext, false);
+ }
/**
* Creates a new {@link ExposedFieldsAggregationOperationContext} from the given {@link ExposedFields}. Uses the given
* {@link AggregationOperationContext} to perform a mapping to mongo types if necessary.
- *
+ *
* @param exposedFields must not be {@literal null}.
* @param rootContext must not be {@literal null}.
+ * @param additional {@literal true} if the context exposes new fields in addition to the previous ones, e.g. in the
+ * case of a lookup operation, {@literal false} otherwise.
*/
- public ExposedFieldsAggregationOperationContext(ExposedFields exposedFields, AggregationOperationContext rootContext) {
+ public ExposedFieldsAggregationOperationContext(ExposedFields exposedFields, AggregationOperationContext rootContext,
+ boolean additional) {
Assert.notNull(exposedFields, "ExposedFields must not be null!");
Assert.notNull(rootContext, "RootContext must not be null!");
this.exposedFields = exposedFields;
this.rootContext = rootContext;
+ this.additional = additional;
}
/*
@@ -79,7 +99,7 @@ public FieldReference getReference(String name) {
/**
* Returns a {@link FieldReference} to the given {@link Field} with the given {@code name}.
- *
+ *
* @param field may be {@literal null}
* @param name must not be {@literal null}
* @return
@@ -112,6 +132,16 @@ private FieldReference getReference(Field field, String name) {
}
}
+ if (additional) {
+
+ // if no exposed fields found propagate to root context.
+ if (field != null) {
+ return rootContext.getReference(field);
+ } else if (StringUtils.hasText(name)) {
+ return rootContext.getReference(name);
+ }
+ }
+
throw new IllegalArgumentException(String.format("Invalid reference '%s'!", name));
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java
new file mode 100644
index 0000000000..141dff0c80
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.core.aggregation;
+
+import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
+import org.springframework.util.Assert;
+
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBObject;
+
+/**
+ * Encapsulates the aggregation framework {@code $lookup}-operation.
+ * We recommend to use the static factory method {@link Aggregation#lookup(String, String, String, String)} instead of
+ * creating instances of this class directly.
+ *
+ * @author Alessio Fachechi
+ * @see http://docs.mongodb.org/manual/reference/aggregation/lookup/#stage._S_lookup
+ * @since 1.9
+ */
+public class LookupOperation implements AdditionalFieldsExposingAggregationOperation {
+
+ private ExposedField from;
+ private ExposedField localField;
+ private ExposedField foreignField;
+ private ExposedField as;
+
+ /**
+ * Creates a new {@link LookupOperation} for the given {@link Field}s.
+ *
+ * @param from must not be {@literal null}.
+ * @param localField must not be {@literal null}.
+ * @param foreignField must not be {@literal null}.
+ * @param as must not be {@literal null}.
+ */
+ public LookupOperation(Field from, Field localField, Field foreignField, Field as) {
+ Assert.notNull(from, "From must not be null!");
+ Assert.notNull(localField, "LocalField must not be null!");
+ Assert.notNull(foreignField, "ForeignField must not be null!");
+ Assert.notNull(as, "As must not be null!");
+
+ this.from = new ExposedField(from, true);
+ this.localField = new ExposedField(localField, true);
+ this.foreignField = new ExposedField(foreignField, true);
+ this.as = new ExposedField(as, true);
+ }
+
+ @Override
+ public ExposedFields getFields() {
+ return ExposedFields.from(as);
+ }
+
+ @Override
+ public DBObject toDBObject(AggregationOperationContext context) {
+ BasicDBObject lookupObject = new BasicDBObject();
+
+ lookupObject.append("from", from.getTarget());
+ lookupObject.append("localField", localField.getTarget());
+ lookupObject.append("foreignField", foreignField.getTarget());
+ lookupObject.append("as", as.getTarget());
+
+ return new BasicDBObject("$lookup", lookupObject);
+ }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/LookupOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/LookupOperationUnitTests.java
new file mode 100644
index 0000000000..1a9bc175ca
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/LookupOperationUnitTests.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.core.aggregation;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import com.mongodb.DBObject;
+import org.junit.Test;
+import org.springframework.data.mongodb.core.DBObjectTestUtils;
+
+/**
+ * Unit tests for {@link LookupOperation}.
+ *
+ * @author Alessio Fachechi
+ */
+public class LookupOperationUnitTests {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void rejectsNullFields() {
+ new LookupOperation((Field) null, (Field) null, (Field) null, (Field) null);
+ }
+
+ @Test
+ public void lookupOperationWithValues() {
+
+ LookupOperation lookupOperation = Aggregation.lookup("a", "b", "c", "d");
+
+ DBObject lookupClause = extractDbObjectFromLookupOperation(lookupOperation);
+
+ assertThat((String) lookupClause.get("from"), is(new String("a")));
+ assertThat((String) lookupClause.get("localField"), is(new String("b")));
+ assertThat((String) lookupClause.get("foreignField"), is(new String("c")));
+ assertThat((String) lookupClause.get("as"), is(new String("d")));
+
+ assertThat(lookupOperation.getFields().exposesNoFields(), is(false));
+ assertThat(lookupOperation.getFields().exposesSingleFieldOnly(), is(true));
+ }
+
+ private DBObject extractDbObjectFromLookupOperation(LookupOperation lookupOperation) {
+ DBObject dbObject = lookupOperation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ DBObject lookupClause = DBObjectTestUtils.getAsDBObject(dbObject, "$lookup");
+ return lookupClause;
+ }
+}