From 3dc6cab13284ddc54e6732513fb5c9d19cf7af48 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 14 Aug 2018 09:39:50 +0200 Subject: [PATCH] DATAMONGO-2053 - Add support for $mergeObjects aggregation operator. Original pull request: #601. --- .../AbstractAggregationExpression.java | 5 + .../core/aggregation/ObjectOperators.java | 228 ++++++++++++++++++ .../aggregation/ObjectOperatorsUnitTests.java | 91 +++++++ src/main/asciidoc/reference/mongodb.adoc | 4 + 4 files changed, 328 insertions(+) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java index ec0a8ff8f..ecdea2166 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import org.bson.Document; +import org.springframework.data.mongodb.core.aggregation.Aggregation.SystemVariable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -92,6 +93,10 @@ abstract class AbstractAggregationExpression implements AggregationExpression { return targetDocument; } + if(value instanceof SystemVariable) { + return value.toString(); + } + return value; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java new file mode 100644 index 000000000..954d915cb --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java @@ -0,0 +1,228 @@ +/* + * Copyright 2018 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.Arrays; +import java.util.Collection; + +import org.bson.Document; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstraction for + * object + * expression operators. + * + * @author Christoph Strobl + * @since 2.1 + */ +public class ObjectOperators { + + /** + * Take the value referenced by given {@literal fieldReference}. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link ObjectOperatorFactory}. + */ + public static ObjectOperatorFactory valueOf(String fieldReference) { + return new ObjectOperatorFactory(Fields.field(fieldReference)); + } + + /** + * Take the value provided by the given {@link AggregationExpression}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link ObjectOperatorFactory}. + */ + public static ObjectOperatorFactory valueOf(AggregationExpression expression) { + return new ObjectOperatorFactory(expression); + } + + /** + * @author Christoph Strobl + */ + public static class ObjectOperatorFactory { + + @Nullable private final Object value; + + /** + * Creates new {@link ObjectOperatorFactory} for given {@literal value}. + * + * @param value must not be {@literal null}. + */ + public ObjectOperatorFactory(Object value) { + + Assert.notNull(value, "Value must not be null!"); + this.value = value; + } + + /** + * Creates new {@link MergeObjects aggregation expression} that takes the associated value and uses + * {@literal $mergeObjects} as an accumulator within the {@literal $group} stage.
+ * NOTE: Requires MongoDB 4.0 or later. + * + * @return new instance of {@link MergeObjects}. + */ + public MergeObjects merge() { + return MergeObjects.merge(value); + } + + /** + * Creates new {@link MergeObjects aggregation expression} that takes the associated value and combines it with the + * given values into a single document.
+ * NOTE: Requires MongoDB 4.0 or later. + * + * @return new instance of {@link MergeObjects}. + */ + public MergeObjects mergeWith(Object... values) { + return merge().mergeWith(values); + } + + /** + * Creates new {@link MergeObjects aggregation expression} that takes the associated value and combines it with the + * values of the given {@link Field field references} into a single document.
+ * NOTE: Requires MongoDB 4.0 or later. + * + * @return new instance of {@link MergeObjects}. + */ + public MergeObjects mergeWithValuesOf(String... fieldReferences) { + return merge().mergeWithValuesOf(fieldReferences); + } + + /** + * Creates new {@link MergeObjects aggregation expression} that takes the associated value and combines it with the + * result values of the given {@link Aggregation expressions} into a single document.
+ * NOTE: Requires MongoDB 4.0 or later. + * + * @return new instance of {@link MergeObjects}. + */ + public MergeObjects mergeWithValuesOf(AggregationExpression... expression) { + return merge().mergeWithValuesOf(expression); + } + } + + /** + * {@link AggregationExpression} for {@code $mergeObjects} that combines multiple documents into a single document. + *
+ * NOTE: Requires MongoDB 4.0 or later. + * + * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/mergeObjects/ + * @since 2.1 + */ + public static class MergeObjects extends AbstractAggregationExpression { + + private MergeObjects(Object value) { + super(value); + } + + /** + * Creates new {@link MergeObjects aggregation expression} that takes given values and combines them into a single + * document.
+ * + * @param values must not be {@literal null}. + * @return new instance of {@link MergeObjects}. + */ + public static MergeObjects merge(Object... values) { + return new MergeObjects(Arrays.asList(values)); + } + + /** + * Creates new {@link MergeObjects aggregation expression} that takes the given {@link Field field references} and + * combines them into a single document. + * + * @param fieldReferences must not be {@literal null}. + * @return new instance of {@link MergeObjects}. + */ + public static MergeObjects mergeValuesOf(String... fieldReferences) { + return merge(Arrays.stream(fieldReferences).map(Fields::field).toArray()); + } + + /** + * Creates new {@link MergeObjects aggregation expression} that takes the result of the given {@link Aggregation + * expressions} and combines them into a single document. + * + * @param expressions must not be {@literal null}. + * @return new instance of {@link MergeObjects}. + */ + public static MergeObjects mergeValuesOf(AggregationExpression... expressions) { + return merge(expressions); + } + + /** + * Creates new {@link MergeObjects aggregation expression} by adding the given {@link Field field references}. + * + * @param fieldReferences must not be {@literal null}. + * @return new instance of {@link MergeObjects}. + */ + public MergeObjects mergeWithValuesOf(String... fieldReferences) { + return mergeWith(Arrays.stream(fieldReferences).map(Fields::field).toArray()); + } + + /** + * Creates new {@link MergeObjects aggregation expression} by adding the given {@link AggregationExpression + * expressions}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link MergeObjects}. + */ + public MergeObjects mergeWithValuesOf(AggregationExpression... expression) { + return mergeWith(expression); + } + + /** + * Creates new {@link MergeObjects aggregation expression} by adding the given values. + * + * @param values must not be {@literal null}. + * @return new instance of {@link MergeObjects}. + */ + public MergeObjects mergeWith(Object... values) { + return new MergeObjects(append(Arrays.asList(values))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#toDocument(java.lang.Object, org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toDocument(Object value, AggregationOperationContext context) { + return super.toDocument(potentiallyExtractSingleValue(value), context); + } + + private Object potentiallyExtractSingleValue(Object value) { + + if (value instanceof Collection) { + + Collection collection = ((Collection) value); + if (collection.size() == 1) { + return collection.iterator().next(); + } + } + return value; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod() + */ + @Override + protected String getMongoMethod() { + return "$mergeObjects"; + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java new file mode 100644 index 000000000..c1d6d3484 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ObjectOperatorsUnitTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2018 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.assertj.core.api.Assertions.*; + +import org.bson.Document; +import org.junit.Test; +import org.springframework.data.mongodb.core.aggregation.Aggregation.SystemVariable; +import org.springframework.data.mongodb.core.aggregation.ObjectOperators.MergeObjects; + +/** + * Unit tests for {@link ObjectOperators}. + * + * @author Christoph Strobl + * @currentRead Royal Assassin - Robin Hobb + */ +public class ObjectOperatorsUnitTests { + + static final String EXPRESSION_STRING = "{ \"$king-in-waiting\" : \"verity\" }"; + static final Document EXPRESSION_DOC = Document.parse(EXPRESSION_STRING); + static final AggregationExpression EXPRESSION = context -> EXPRESSION_DOC; + + @Test // DATAMONGO-2053 + public void mergeSingleFieldReference() { + + assertThat(ObjectOperators.valueOf("kettricken").merge().toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $mergeObjects: \"$kettricken\" } ")); + } + + @Test // DATAMONGO-2053 + public void mergeSingleExpression() { + + assertThat(ObjectOperators.valueOf(EXPRESSION).merge().toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $mergeObjects: " + EXPRESSION_STRING + " } ")); + } + + @Test // DATAMONGO-2053 + public void mergeEmpty() { + + assertThat(MergeObjects.merge().toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $mergeObjects: [] } ")); + } + + @Test // DATAMONGO-2053 + public void mergeMuliFieldReference() { + + assertThat( + ObjectOperators.valueOf("kettricken").mergeWithValuesOf("verity").toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $mergeObjects: [ \"$kettricken\", \"$verity\" ] } ")); + } + + @Test // DATAMONGO-2053 + public void mergeMixed() { + + assertThat( + ObjectOperators.valueOf("kettricken").mergeWithValuesOf(EXPRESSION).toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $mergeObjects: [ \"$kettricken\", " + EXPRESSION_STRING + " ] } ")); + } + + @Test // DATAMONGO-2053 + public void mergeWithSystemVariable() { + + assertThat( + ObjectOperators.valueOf(EXPRESSION).mergeWith(SystemVariable.ROOT).toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $mergeObjects: [ " + EXPRESSION_STRING + ", \"$$ROOT\" ] } ")); + } + + @Test // DATAMONGO-2053 + public void mergeMany() { + + assertThat(ObjectOperators.valueOf("kettricken").mergeWithValuesOf(EXPRESSION) + .mergeWith(new Document("fitz", "chivalry")).toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse( + "{ $mergeObjects: [ \"$kettricken\", " + EXPRESSION_STRING + ", { \"fitz\" : \"chivalry\" } ] } ")); + } + +} diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index ebcf2314f..4e69bf26a 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -2100,6 +2100,7 @@ The MongoDB Aggregation Framework provides the following types of aggregation op * Conditional Aggregation Operators * Lookup Aggregation Operators * Convert Aggregation Operators +* Object Aggregation Operators At the time of this writing, we provide support for the following Aggregation Operations in Spring Data MongoDB: @@ -2144,6 +2145,9 @@ At the time of this writing, we provide support for the following Aggregation Op | Convert Aggregation Operators | `convert`, `toBool`, `toDate`, `toDecimal`, `toDouble`, `toInt`, `toLong`, `toObjectId`, `toString` + +| Object Aggregation Operators +| `mergeObjects` |=== * The operation is mapped or added by Spring Data MongoDB.