From 510028a834ff585e8805774a717bf94c56d009d8 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 23 Jul 2021 11:33:07 +0200 Subject: [PATCH] Add support for `$shift` aggregation Operator. Closes: #3727 Original pull request: #3741. --- .../core/aggregation/DocumentOperators.java | 124 +++++++++++++++++- .../core/spel/MethodReferenceNode.java | 1 + .../DocumentOperatorsUnitTests.java | 17 ++- .../SpelExpressionTransformerUnitTests.java | 14 ++ 4 files changed, 153 insertions(+), 3 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java index 7a5918e04..76fa591e4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.core.aggregation; +import java.util.Collections; + import org.bson.Document; /** @@ -45,6 +47,26 @@ public class DocumentOperators { return new DenseRank(); } + /** + * Take the field referenced by given {@literal fieldReference}. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link DocumentOperatorsFactory}. + */ + public static DocumentOperatorsFactory valueOf(String fieldReference) { + return new DocumentOperatorsFactory(fieldReference); + } + + /** + * Take the value resulting from the given {@link AggregationExpression}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link DocumentOperatorsFactory}. + */ + public static DocumentOperatorsFactory valueOf(AggregationExpression expression) { + return new DocumentOperatorsFactory(expression); + } + /** * Obtain the current document position. * @@ -55,6 +77,35 @@ public class DocumentOperators { return new DocumentNumber(); } + /** + * @author Christoph Strobl + */ + public static class DocumentOperatorsFactory { + + private Object target; + + public DocumentOperatorsFactory(Object target) { + this.target = target; + } + + /** + * Creates new {@link AggregationExpression} that applies the expression to a document at specified position + * relative to the current document. + * + * @param by the value to add to the current position. + * @return new instance of {@link Shift}. + */ + public Shift shift(int by) { + + Shift shift = usesExpression() ? Shift.shift((AggregationExpression) target) : Shift.shift(target.toString()); + return shift.by(by); + } + + private boolean usesExpression() { + return target instanceof AggregationExpression; + } + } + /** * {@link Rank} resolves the current document position (the rank) relative to other documents. If multiple documents * occupy the same rank, {@literal $rank} places the document with the subsequent value at a rank with a gap. @@ -72,8 +123,8 @@ public class DocumentOperators { /** * {@link DenseRank} resolves the current document position (the rank) relative to other documents. If multiple - * documents occupy the same rank, {@literal $denseRank} places the document with the subsequent value at the next rank without - * any gaps. + * documents occupy the same rank, {@literal $denseRank} places the document with the subsequent value at the next + * rank without any gaps. * * @author Christoph Strobl * @since 3.3 @@ -99,4 +150,73 @@ public class DocumentOperators { return new Document("$documentNumber", new Document()); } } + + /** + * Shift applies an expression to a document in a specified position relative to the current document. + * + * @author Christoph Strobl + * @since 3.3 + */ + public static class Shift extends AbstractAggregationExpression { + + private Shift(Object value) { + super(value); + } + + /** + * Specifies the field to evaluate and return. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link Shift}. + */ + public static Shift shift(String fieldReference) { + return new Shift(Collections.singletonMap("output", Fields.field(fieldReference))); + } + + /** + * Specifies the {@link AggregationExpression expression} to evaluate and return. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Shift}. + */ + public static Shift shift(AggregationExpression expression) { + return new Shift(Collections.singletonMap("output", expression)); + } + + /** + * Shift the document position relative to the current. Use a positive value for follow up documents (eg. 1 for the + * next) or a negative value for the predecessor documents (eg. -1 for the previous). + * + * @param shiftBy value to add to the current position. + * @return new instance of {@link Shift}. + */ + public Shift by(int shiftBy) { + return new Shift(append("by", shiftBy)); + } + + /** + * Define the default value if the target document is out of range. + * + * @param value must not be {@literal null}. + * @return new instance of {@link Shift}. + */ + public Shift defaultTo(Object value) { + return new Shift(append("default", value)); + } + + /** + * Define the {@link AggregationExpression expression} to evaluate if the target document is out of range. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Shift}. + */ + public Shift defaultToValueOf(AggregationExpression expression) { + return defaultTo(expression); + } + + @Override + protected String getMongoMethod() { + return "$shift"; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index a184c49be..4052b2cba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -72,6 +72,7 @@ public class MethodReferenceNode extends ExpressionNode { map.put("rank", emptyRef().forOperator("$rank")); map.put("denseRank", emptyRef().forOperator("$denseRank")); map.put("documentNumber", emptyRef().forOperator("$documentNumber")); + map.put("shift", mapArgRef().forOperator("$shift").mappingParametersTo("output", "by", "default")); // ARITHMETIC OPERATORS map.put("abs", singleArgRef().forOperator("$abs")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DocumentOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DocumentOperatorsUnitTests.java index c6604bc54..4a29db60d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DocumentOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DocumentOperatorsUnitTests.java @@ -39,6 +39,21 @@ class DocumentOperatorsUnitTests { @Test // GH-3717 void rendersDocumentNumber() { - assertThat(documentNumber().toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo(new Document("$documentNumber", new Document())); + assertThat(documentNumber().toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(new Document("$documentNumber", new Document())); + } + + @Test // GH-3727 + void rendersShift() { + + assertThat(valueOf("quantity").shift(1).toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $shift: { output: \"$quantity\", by: 1 } }")); + } + + @Test // GH-3727 + void rendersShiftWithDefault() { + + assertThat(valueOf("quantity").shift(1).defaultTo("Not available").toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $shift: { output: \"$quantity\", by: 1, default: \"Not available\" } }")); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index 3c20b58d5..334825a82 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -971,6 +971,20 @@ public class SpelExpressionTransformerUnitTests { assertThat(transform("documentNumber()")).isEqualTo(Document.parse("{ $documentNumber : {} }")); } + @Test // GH-3727 + void rendersShift() { + + assertThat(transform("shift(quantity, 1)")) + .isEqualTo(Document.parse("{ $shift: { output: \"$quantity\", by: 1 } }")); + } + + @Test // GH-3727 + void rendersShiftWithDefault() { + + assertThat(transform("shift(quantity, 1, 'Not available')")) + .isEqualTo(Document.parse("{ $shift: { output: \"$quantity\", by: 1, default: \"Not available\" } }")); + } + private Object transform(String expression, Object... params) { Object result = transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); return result == null ? null : (!(result instanceof org.bson.Document) ? result.toString() : result);