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
+ * 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