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 21d25ef17..a6afc39c8 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 @@ -29,6 +29,8 @@ import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFie 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.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; +import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootDocumentOperationBuilder; +import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootOperationBuilder; import org.springframework.data.mongodb.core.aggregation.Fields.*; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.CriteriaDefinition; @@ -216,6 +218,40 @@ public class Aggregation { return new UnwindOperation(field(field)); } + /** + * Factory method to create a new {@link ReplaceRootOperation} for the field with the given name. + * + * @param fieldName must not be {@literal null} or empty. + * @return + * @since 1.10 + */ + public static ReplaceRootOperation replaceRoot(String fieldName) { + return ReplaceRootOperation.builder().withValueOf(fieldName); + } + + /** + * Factory method to create a new {@link ReplaceRootOperation} for the field with the given + * {@link AggregationExpression}. + * + * @param aggregationExpression must not be {@literal null}. + * @return + * @since 1.10 + */ + public static ReplaceRootOperation replaceRoot(AggregationExpression aggregationExpression) { + return ReplaceRootOperation.builder().withValueOf(aggregationExpression); + } + + /** + * Factory method to create a new {@link ReplaceRootDocumentOperationBuilder} to configure a + * {@link ReplaceRootOperation}. + * + * @return the {@literal ReplaceRootDocumentOperationBuilder}. + * @since 1.10 + */ + public static ReplaceRootOperationBuilder replaceRoot() { + return ReplaceRootOperation.builder(); + } + /** * Factory method to create a new {@link UnwindOperation} for the field with the given name and * {@code preserveNullAndEmptyArrays}. Note that extended unwind is supported in MongoDB version 3.2+. @@ -468,11 +504,13 @@ public class Aggregation { if (operation instanceof FieldsExposingAggregationOperation) { FieldsExposingAggregationOperation exposedFieldsOperation = (FieldsExposingAggregationOperation) operation; + ExposedFields fields = exposedFieldsOperation.getFields(); if (operation instanceof InheritsFieldsAggregationOperation) { - context = new InheritingExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), context); + context = new InheritingExposedFieldsAggregationOperationContext(fields, context); } else { - context = new ExposedFieldsAggregationOperationContext(exposedFieldsOperation.getFields(), context); + context = fields.exposesNoFields() ? DEFAULT_CONTEXT + : new ExposedFieldsAggregationOperationContext(fields, context); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java new file mode 100644 index 000000000..cdf5c30c0 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java @@ -0,0 +1,562 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.bson.Document; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.expression.spel.ast.Projection; +import org.springframework.util.Assert; + +/** + * Encapsulates the aggregation framework {@code $replaceRoot}-operation. + *

+ * We recommend to use the static factory method {@link Aggregation#replaceRoot(String)} instead of creating instances + * of this class directly. + * + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/replaceRoot/#pipe._S_replaceRoot + * @author Mark Paluch + * @since 1.10 + */ +public class ReplaceRootOperation implements FieldsExposingAggregationOperation { + + private final Replacement replacement; + + /** + * Creates a new {@link ReplaceRootOperation} given the {@link as} field name. + * + * @param field must not be {@literal null} or empty. + */ + public ReplaceRootOperation(Field field) { + this.replacement = new FieldReplacement(field); + } + + /** + * Creates a new {@link ReplaceRootOperation} given the {@link as} field name. + * + * @param aggregationExpression must not be {@literal null}. + */ + public ReplaceRootOperation(AggregationExpression aggregationExpression) { + + Assert.notNull(aggregationExpression, "AggregationExpression must not be null!"); + this.replacement = new AggregationExpressionReplacement(aggregationExpression); + } + + protected ReplaceRootOperation(Replacement replacement) { + this.replacement = replacement; + } + + /** + * Creates a new {@link ReplaceRootDocumentOperationBuilder}. + * + * @return a new {@link ReplaceRootDocumentOperationBuilder}. + */ + public static ReplaceRootOperationBuilder builder() { + return new ReplaceRootOperationBuilder(); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toDocument(AggregationOperationContext context) { + return new Document("$replaceRoot", new Document("newRoot", replacement.toObject(context))); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields() + */ + @Override + public ExposedFields getFields() { + return ExposedFields.from(); + } + + /** + * Builder for {@link ReplaceRootOperation}. + * + * @author Mark Paluch + */ + public static class ReplaceRootOperationBuilder { + + /** + * Defines a root document replacement based on a {@literal fieldName} that resolves to a document. + * + * @param fieldName must not be {@literal null} or empty. + * @return the final {@link ReplaceRootOperation}. + */ + public ReplaceRootOperation withValueOf(String fieldName) { + return new ReplaceRootOperation(Fields.field(fieldName)); + } + + /** + * Defines a root document replacement based on a {@link AggregationExpression} that resolves to a document. + * + * @param aggregationExpression must not be {@literal null}. + * @return the final {@link ReplaceRootOperation}. + */ + public ReplaceRootOperation withValueOf(AggregationExpression aggregationExpression) { + return new ReplaceRootOperation(aggregationExpression); + } + + /** + * Defines a root document replacement based on a composable document that is empty initially. + *

+ * {@link ReplaceRootOperation} can be populated with individual entries and derive its values from other, existing + * documents. + * + * @return the {@link ReplaceRootDocumentOperation}. + */ + public ReplaceRootDocumentOperation withDocument() { + return new ReplaceRootDocumentOperation(); + } + + /** + * Defines a root document replacement based on a composable document given {@literal document} + *

+ * {@link ReplaceRootOperation} can be populated with individual entries and derive its values from other, existing + * documents. + * + * @param document must not be {@literal null}. + * @return the final {@link ReplaceRootOperation}. + */ + public ReplaceRootOperation withDocument(Document document) { + + Assert.notNull(document, "Document must not be null!"); + + return new ReplaceRootDocumentOperation().andValuesOf(document); + } + } + + /** + * Encapsulates the aggregation framework {@code $replaceRoot}-operation to result in a composable replacement + * document. + *

+ * Instances of {@link ReplaceRootDocumentOperation} yield empty upon construction and can be populated with single + * values and documents. + * + * @author Mark Paluch + */ + static class ReplaceRootDocumentOperation extends ReplaceRootOperation { + + private final static ReplacementDocument EMPTY = new ReplacementDocument(); + private final ReplacementDocument current; + + /** + * Creates an empty {@link ReplaceRootDocumentOperation}. + */ + public ReplaceRootDocumentOperation() { + this(EMPTY); + } + + private ReplaceRootDocumentOperation(ReplacementDocument replacementDocument) { + super(replacementDocument); + current = replacementDocument; + } + + /** + * Creates an extended {@link ReplaceRootDocumentOperation} that combines {@link ReplacementDocument}s from the + * {@literal currentOperation} and {@literal extension} operation. + * + * @param currentOperation must not be {@literal null}. + * @param extension must not be {@literal null}. + */ + protected ReplaceRootDocumentOperation(ReplaceRootDocumentOperation currentOperation, + ReplacementDocument extension) { + this(currentOperation.current.extendWith(extension)); + } + + /** + * Creates a new {@link ReplaceRootDocumentOperationBuilder} to define a field for the {@link AggregationExpression} + * . + * + * @param aggregationExpression must not be {@literal null}. + * @return the {@link ReplaceRootDocumentOperationBuilder}. + */ + public ReplaceRootDocumentOperationBuilder and(AggregationExpression aggregationExpression) { + return new ReplaceRootDocumentOperationBuilder(this, aggregationExpression); + } + + /** + * Creates a new {@link ReplaceRootDocumentOperationBuilder} to define a field for the {@literal value}. + * + * @param value must not be {@literal null}. + * @return the {@link ReplaceRootDocumentOperationBuilder}. + */ + public ReplaceRootDocumentOperationBuilder andValue(Object value) { + return new ReplaceRootDocumentOperationBuilder(this, value); + } + + /** + * Creates a new {@link ReplaceRootDocumentOperation} that merges all existing replacement values with values from + * {@literal value}. Existing replacement values are overwritten. + * + * @param value must not be {@literal null}. + * @return the {@link ReplaceRootDocumentOperation}. + */ + public ReplaceRootDocumentOperation andValuesOf(Object value) { + return new ReplaceRootDocumentOperation(this, ReplacementDocument.valueOf(value)); + } + } + + /** + * Builder for {@link ReplaceRootDocumentOperation} to populate {@link ReplacementDocument} + * + * @author Mark Paluch + */ + public static class ReplaceRootDocumentOperationBuilder { + + private final ReplaceRootDocumentOperation currentOperation; + private final Object value; + + protected ReplaceRootDocumentOperationBuilder(ReplaceRootDocumentOperation currentOperation, Object value) { + + Assert.notNull(currentOperation, "Current ReplaceRootDocumentOperation must not be null!"); + Assert.notNull(value, "Value must not be null!"); + + this.currentOperation = currentOperation; + this.value = value; + } + + public ReplaceRootDocumentOperation as(String fieldName) { + + if (value instanceof AggregationExpression) { + return new ReplaceRootDocumentOperation(currentOperation, + ReplacementDocument.forExpression(fieldName, (AggregationExpression) value)); + } + + return new ReplaceRootDocumentOperation(currentOperation, ReplacementDocument.forSingleValue(fieldName, value)); + } + } + + /** + * Replacement object that results in a replacement document or an expression that results in a document. + * + * @author Mark Paluch + */ + private abstract static class Replacement { + + /** + * Renders the current {@link Replacement} into a {@link Document} based on the given + * {@link AggregationOperationContext}. + * + * @param context will never be {@literal null}. + * @return a replacement document or an expression that results in a document. + */ + public abstract Object toObject(AggregationOperationContext context); + } + + /** + * {@link Replacement} that uses a {@link AggregationExpression} that results in a replacement document. + * + * @author Mark Paluch + */ + private static class AggregationExpressionReplacement extends Replacement { + + private final AggregationExpression aggregationExpression; + + protected AggregationExpressionReplacement(AggregationExpression aggregationExpression) { + this.aggregationExpression = aggregationExpression; + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.Replacement#toObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toObject(AggregationOperationContext context) { + return aggregationExpression.toDocument(context); + } + } + + /** + * {@link Replacement that references a {@link Field} inside the current aggregation pipeline. + * + * @author Mark Paluch + */ + private static class FieldReplacement extends Replacement { + + private final Field field; + + /** + * Creates {@link FieldReplacement} given {@link Field}. + */ + protected FieldReplacement(Field field) { + + Assert.notNull(field, "Field must not be null!"); + this.field = field; + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.Replacement#toObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Object toObject(AggregationOperationContext context) { + return context.getReference(field).toString(); + } + } + + /** + * Replacement document consisting of multiple {@link ReplacementContributor}s. + * + * @author Mark Paluch + */ + private static class ReplacementDocument extends Replacement { + + private final Collection replacements; + + /** + * Creates an empty {@link ReplacementDocument}. + */ + protected ReplacementDocument() { + replacements = new ArrayList(); + } + + /** + * Creates a {@link ReplacementDocument} given {@link ReplacementContributor}. + * + * @param contributor + */ + protected ReplacementDocument(ReplacementContributor contributor) { + + Assert.notNull(contributor, "ReplacementContributor must not be null!"); + replacements = Collections.singleton(contributor); + } + + private ReplacementDocument(Collection replacements) { + this.replacements = replacements; + } + + /** + * Creates a {@link ReplacementDocument} given a {@literal value}. + * + * @param value must not be {@literal null}. + * @return + */ + public static ReplacementDocument valueOf(Object value) { + return new ReplacementDocument(new DocumentContributor(value)); + } + + /** + * Creates a {@link ReplacementDocument} given a single {@literal field} and {@link AggregationExpression}. + * + * @param aggregationExpression must not be {@literal null}. + * @return + */ + public static ReplacementDocument forExpression(String field, AggregationExpression aggregationExpression) { + return new ReplacementDocument(new ExpressionFieldContributor(Fields.field(field), aggregationExpression)); + } + + /** + * Creates a {@link ReplacementDocument} given a single {@literal field} and {@literal value}. + * + * @param value must not be {@literal null}. + * @return + */ + public static ReplacementDocument forSingleValue(String field, Object value) { + return new ReplacementDocument(new ValueFieldContributor(Fields.field(field), value)); + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.Replacement#toObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toObject(AggregationOperationContext context) { + + Document document = new Document(); + + for (ReplacementContributor replacement : replacements) { + document.putAll(replacement.toDocument(context)); + } + + return document; + } + + /** + * Extend a replacement document that merges {@code this} and {@literal replacement} {@link ReplacementContributor}s + * in a new {@link ReplacementDocument}. + * + * @param extension must not be {@literal null}. + * @return the new, extended {@link ReplacementDocument} + */ + public ReplacementDocument extendWith(ReplacementDocument extension) { + + Assert.notNull(extension, "ReplacementDocument must not be null"); + + ReplacementDocument replacementDocument = new ReplacementDocument(); + + List replacements = new ArrayList( + this.replacements.size() + replacementDocument.replacements.size()); + + replacements.addAll(this.replacements); + replacements.addAll(extension.replacements); + + return new ReplacementDocument(replacements); + } + } + + /** + * Partial {@link Document} contributor for document replacement. + * + * @author Mark Paluch + */ + private abstract static class ReplacementContributor { + + /** + * Renders the current {@link ReplacementContributor} into a {@link Document} based on the given + * {@link AggregationOperationContext}. + * + * @param context will never be {@literal null}. + * @return + */ + public abstract Document toDocument(AggregationOperationContext context); + } + + /** + * {@link ReplacementContributor} to contribute multiple fields based on the input {@literal value}. + *

+ * The value object is mapped into a MongoDB {@link Document}. + * + * @author Mark Paluch + */ + private static class DocumentContributor extends ReplacementContributor { + + private final Object value; + + /** + * Creates new {@link Projection} for the given {@link Field}. + * + * @param value must not be {@literal null}. + */ + public DocumentContributor(Object value) { + + Assert.notNull(value, "Value must not be null!"); + this.value = value; + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplacementContributor#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toDocument(AggregationOperationContext context) { + + Document document = new Document("$set", value); + + return (Document) context.getMappedObject(document).get("$set"); + } + } + + /** + * Base class for {@link ReplacementContributor} implementations to contribute a single {@literal field} Typically + * used to construct a composite document that should contain the resulting key-value pair. + * + * @author Mark Paluch + */ + private abstract static class FieldContributorSupport extends ReplacementContributor { + + private final ExposedField field; + + /** + * Creates new {@link FieldContributorSupport} for the given {@link Field}. + * + * @param field must not be {@literal null}. + */ + public FieldContributorSupport(Field field) { + + Assert.notNull(field, "Field must not be null!"); + this.field = new ExposedField(field, true); + } + + /** + * @return the {@link ExposedField}. + */ + public ExposedField getField() { + return field; + } + } + + /** + * {@link ReplacementContributor} to contribute a single {@literal field} and {@literal value}. The {@literal value} + * is mapped to a MongoDB {@link Document} and can be a singular value, a list or subdocument. + * + * @author Mark Paluch + */ + private static class ValueFieldContributor extends FieldContributorSupport { + + private final Object value; + + /** + * Creates new {@link Projection} for the given {@link Field}. + * + * @param field must not be {@literal null}. + * @param value must not be {@literal null}. + */ + public ValueFieldContributor(Field field, Object value) { + + super(field); + + Assert.notNull(value, "Value must not be null!"); + + this.value = value; + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplacementContributor#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toDocument(AggregationOperationContext context) { + + Document document = new Document("$set", value); + return new Document(getField().getTarget(), context.getMappedObject(document).get("$set")); + } + } + + /** + * {@link ReplacementContributor} to contribute a single {@literal field} and value based on a + * {@link AggregationExpression}. + * + * @author Mark Paluch + */ + private static class ExpressionFieldContributor extends FieldContributorSupport { + + private final AggregationExpression aggregationExpression; + + /** + * Creates new {@link Projection} for the given {@link Field}. + * + * @param field must not be {@literal null}. + * @param aggregationExpression must not be {@literal null}. + */ + public ExpressionFieldContributor(Field field, AggregationExpression aggregationExpression) { + + super(field); + + Assert.notNull(aggregationExpression, "AggregationExpression must not be null!"); + + this.aggregationExpression = aggregationExpression; + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplacementContributor#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toDocument(AggregationOperationContext context) { + return new Document(getField().getTarget(), aggregationExpression.toDocument(context)); + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index aa77b53e3..8b0a29357 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java @@ -1052,6 +1052,32 @@ public class AggregationTests { assertThat((Integer) document.get("millisecond"), is(789)); } + /** + * @see DATAMONGO-1550 + */ + @Test + public void shouldPerformReplaceRootOperatorCorrectly() throws ParseException { + + assumeTrue(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_FOUR)); + + Data data = new Data(); + DataItem dataItem = new DataItem(); + dataItem.primitiveIntValue = 42; + data.item = dataItem; + mongoTemplate.insert(data); + + TypedAggregation agg = newAggregation(Data.class, project("item"), // + replaceRoot("item"), // + project().and("primitiveIntValue").as("my_primitiveIntValue")); + + AggregationResults results = mongoTemplate.aggregate(agg, Document.class); + Document resultDocument = results.getUniqueMappedResult(); + + assertThat(resultDocument, is(notNullValue())); + assertThat((Integer) resultDocument.get("my_primitiveIntValue"), is(42)); + assertThat((Integer) resultDocument.keySet().size(), is(1)); + } + /** * @see DATAMONGO-788 */ diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java index 75cd09f61..1c9e93af5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java @@ -155,6 +155,21 @@ public class AggregationUnitTests { isBsonObject().notContaining("includeArrayIndex").containing("preserveNullAndEmptyArrays", true)); } + /** + * @see DATAMONGO-1550 + */ + @Test + public void replaceRootOperationShouldBuildCorrectClause() { + + Document agg = newAggregation( // + replaceRoot().withDocument().andValue("value").as("field")) // + .toDocument("foo", Aggregation.DEFAULT_CONTEXT); + + @SuppressWarnings("unchecked") + Document unwind = ((List) agg.get("pipeline")).get(0); + assertThat(unwind, isBsonObject().containing("$replaceRoot.newRoot", new Document("field", "value"))); + } + /** * @see DATAMONGO-753 */ diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperationUnitTests.java new file mode 100644 index 000000000..2aa30caec --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperationUnitTests.java @@ -0,0 +1,127 @@ +/* + * 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 org.bson.Document; +import org.junit.Test; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.VariableOperators; +import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootDocumentOperation; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import com.mongodb.util.JSON; + +/** + * Unit tests for {@link ReplaceRootOperation}. + * + * @author Mark Paluch + */ +public class ReplaceRootOperationUnitTests { + + /** + * @see DATAMONGO-1550 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullField() { + new ReplaceRootOperation((Field) null); + } + + /** + * @see DATAMONGO-1550 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullExpression() { + new ReplaceRootOperation((AggregationExpression) null); + } + + /** + * @see DATAMONGO-1550 + */ + @Test + public void shouldRenderCorrectly() { + + ReplaceRootOperation operation = ReplaceRootDocumentOperation.builder() + .withDocument(new Document("hello", "world")); + Document dbObject = operation.toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(dbObject, is(Document.parse("{ $replaceRoot : { newRoot: { hello: \"world\" } } }"))); + } + + /** + * @see DATAMONGO-1550 + */ + @Test + public void shouldRenderExpressionCorrectly() { + + ReplaceRootOperation operation = new ReplaceRootOperation(VariableOperators // + .mapItemsOf("array") // + .as("element") // + .andApply(AggregationFunctionExpressions.MULTIPLY.of("$$element", 10))); + + Document dbObject = operation.toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(dbObject, is(Document.parse("{ $replaceRoot : { newRoot : { " + + "$map : { input : \"$array\" , as : \"element\" , in : { $multiply : [ \"$$element\" , 10]} } " + "} } }"))); + } + + /** + * @see DATAMONGO-1550 + */ + @Test + public void shouldComposeDocument() { + + ReplaceRootOperation operation = ReplaceRootDocumentOperation.builder().withDocument() // + .andValue("value").as("key") // + .and(AggregationFunctionExpressions.MULTIPLY.of("$$element", 10)).as("multiply"); + + Document dbObject = operation.toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(dbObject, is(Document + .parse("{ $replaceRoot : { newRoot: { key: \"value\", multiply: { $multiply : [ \"$$element\" , 10]} } } }"))); + } + + /** + * @see DATAMONGO-1550 + */ + @Test + public void shouldComposeSubDocument() { + + Document partialReplacement = new Document("key", "override").append("key2", "value2"); + + ReplaceRootOperation operation = ReplaceRootDocumentOperation.builder().withDocument() // + .andValue("value").as("key") // + .andValuesOf(partialReplacement); + + Document dbObject = operation.toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(dbObject, is(Document.parse("{ $replaceRoot : { newRoot: { key: \"override\", key2: \"value2\"} } } }"))); + } + + /** + * @see DATAMONGO-1550 + */ + @Test + public void shouldNotExposeFields() { + + ReplaceRootOperation operation = new ReplaceRootOperation(Fields.field("field")); + + assertThat(operation.getFields().exposesNoFields(), is(true)); + assertThat(operation.getFields().exposesSingleFieldOnly(), is(false)); + } +} diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 19ee24886..9b3154b17 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1676,7 +1676,7 @@ At the time of this writing we provide support for the following Aggregation Ope [cols="2*"] |=== | Pipeline Aggregation Operators -| project, skip, limit, lookup, unwind, group, sort, geoNear +| count, geoNear, group, limit, lookup, match, project, replaceRoot, skip, sort, unwind | Set Aggregation Operators | setEquals, setIntersection, setUnion, setDifference, setIsSubset, anyElementTrue, allElementsTrue