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 deleted file mode 100644 index 52b851f53..000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AdditionalFieldsExposingAggregationOperation.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 f6f42b6ff..3c2e5d160 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 @@ -272,10 +272,14 @@ public class Aggregation { } /** - * Creates a new {@link LookupOperation} for the given fields. + * Creates a new {@link LookupOperation}. * - * @param fields must not be {@literal null}. - * @return + * @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}. + * @return never {@literal null}. + * @since 1.9 */ public static LookupOperation lookup(String from, String localField, String foreignField, String as) { return lookup(field(from), field(localField), field(foreignField), field(as)); @@ -284,8 +288,12 @@ public class Aggregation { /** * Creates a new {@link LookupOperation} for the given {@link Fields}. * - * @param fields must not be {@literal null}. - * @return + * @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}. + * @return never {@literal null}. + * @since 1.9 */ public static LookupOperation lookup(Field from, Field localField, Field foreignField, Field as) { return new LookupOperation(from, localField, foreignField, as); @@ -352,11 +360,9 @@ public class Aggregation { 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, - additional); + context = new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), rootContext); } } 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 3e23db927..2ddb1a734 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,9 +17,7 @@ package org.springframework.data.mongodb.core.aggregation; 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; @@ -29,14 +27,12 @@ import com.mongodb.DBObject; * * @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 @@ -46,28 +42,13 @@ class ExposedFieldsAggregationOperationContext implements AggregationOperationCo * @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, - boolean additional) { + AggregationOperationContext rootContext) { 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; } /* @@ -132,16 +113,6 @@ class ExposedFieldsAggregationOperationContext implements AggregationOperationCo } } - 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 index 141dff0c8..8e96d62e2 100644 --- 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 @@ -22,19 +22,19 @@ 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. + * 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 + * @author Christoph Strobl * @see http://docs.mongodb.org/manual/reference/aggregation/lookup/#stage._S_lookup * @since 1.9 */ -public class LookupOperation implements AdditionalFieldsExposingAggregationOperation { +public class LookupOperation implements FieldsExposingAggregationOperation { - private ExposedField from; - private ExposedField localField; - private ExposedField foreignField; + private Field from; + private Field localField; + private Field foreignField; private ExposedField as; /** @@ -46,24 +46,38 @@ public class LookupOperation implements AdditionalFieldsExposingAggregationOpera * @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.from = from; + this.localField = localField; + this.foreignField = foreignField; this.as = new ExposedField(as, true); } + private LookupOperation() { + // used by builder + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields() + */ @Override public ExposedFields getFields() { return ExposedFields.from(as); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override public DBObject toDBObject(AggregationOperationContext context) { + BasicDBObject lookupObject = new BasicDBObject(); lookupObject.append("from", from.getTarget()); @@ -73,4 +87,106 @@ public class LookupOperation implements AdditionalFieldsExposingAggregationOpera return new BasicDBObject("$lookup", lookupObject); } + + /** + * Get a builder that allows creation of {@link LookupOperation}. + * + * @return + */ + public static FromBuilder newLookup() { + return new LookupOperationBuilder(); + } + + public static interface FromBuilder { + + /** + * @param name + * @return + */ + LocalFieldBuilder from(String name); + } + + public static interface LocalFieldBuilder { + + /** + * @param name + * @return + */ + ForeignFieldBuilder localField(String name); + } + + public static interface ForeignFieldBuilder { + + /** + * @param name + * @return + */ + AsBuilder foreignField(String name); + } + + public static interface AsBuilder { + + /** + * @param name + * @return + */ + LookupOperation as(String name); + } + + /** + * Builder for fluent {@link LookupOperation} creation. + * + * @author Christoph Strobl + * @since 1.9 + */ + public static final class LookupOperationBuilder + implements FromBuilder, LocalFieldBuilder, ForeignFieldBuilder, AsBuilder { + + private LookupOperation lookupOperation; + + private LookupOperationBuilder() { + this.lookupOperation = new LookupOperation(); + } + + /** + * Creates new builder for {@link LookupOperation}. + * + * @return never {@literal null}. + */ + public static FromBuilder newBuilder() { + return new LookupOperationBuilder(); + } + + @Override + public LocalFieldBuilder from(String name) { + + Assert.hasText(name, "'From' must not be null or empty!"); + lookupOperation.from = Fields.field(name); + return this; + } + + @Override + public LookupOperation as(String name) { + + Assert.hasText(name, "'As' must not be null or empty!"); + lookupOperation.as = new ExposedField(Fields.field(name), true); + return null; + } + + @Override + public AsBuilder foreignField(String name) { + + Assert.hasText(name, "'ForeignField' must not be null or empty!"); + lookupOperation.foreignField = Fields.field(name); + return this; + } + + @Override + public ForeignFieldBuilder localField(String name) { + + Assert.hasText(name, "'LocalField' must not be null or empty!"); + lookupOperation.localField = Fields.field(name); + return this; + } + } } 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 index 1a9bc175c..47dfb2b39 100644 --- 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 @@ -17,23 +17,56 @@ package org.springframework.data.mongodb.core.aggregation; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.springframework.data.mongodb.test.util.IsBsonObject.*; -import com.mongodb.DBObject; import org.junit.Test; import org.springframework.data.mongodb.core.DBObjectTestUtils; +import com.mongodb.DBObject; + /** * Unit tests for {@link LookupOperation}. * * @author Alessio Fachechi + * @author Christoph Strobl */ public class LookupOperationUnitTests { + /** + * @see DATAMONGO-1326 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullForFrom() { + new LookupOperation(null, Fields.field("localField"), Fields.field("foreignField"), Fields.field("as")); + } + + /** + * @see DATAMONGO-1326 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullLocalFieldField() { + new LookupOperation(Fields.field("from"), null, Fields.field("foreignField"), Fields.field("as")); + } + + /** + * @see DATAMONGO-1326 + */ @Test(expected = IllegalArgumentException.class) - public void rejectsNullFields() { - new LookupOperation((Field) null, (Field) null, (Field) null, (Field) null); + public void rejectsNullForeignField() { + new LookupOperation(Fields.field("from"), Fields.field("localField"), null, Fields.field("as")); } + /** + * @see DATAMONGO-1326 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullForAs() { + new LookupOperation(Fields.field("from"), Fields.field("localField"), Fields.field("foreignField"), null); + } + + /** + * @see DATAMONGO-1326 + */ @Test public void lookupOperationWithValues() { @@ -41,16 +74,28 @@ public class LookupOperationUnitTests { 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(lookupClause, + isBsonObject().containing("from", "a") // + .containing("localField", "b") // + .containing("foreignField", "c") // + .containing("as", "d")); + } + + /** + * @see DATAMONGO-1326 + */ + @Test + public void lookupOperationExposesAsField() { + + LookupOperation lookupOperation = Aggregation.lookup("a", "b", "c", "d"); assertThat(lookupOperation.getFields().exposesNoFields(), is(false)); assertThat(lookupOperation.getFields().exposesSingleFieldOnly(), is(true)); + assertThat(lookupOperation.getFields().getField("d"), notNullValue()); } private DBObject extractDbObjectFromLookupOperation(LookupOperation lookupOperation) { + DBObject dbObject = lookupOperation.toDBObject(Aggregation.DEFAULT_CONTEXT); DBObject lookupClause = DBObjectTestUtils.getAsDBObject(dbObject, "$lookup"); return lookupClause;