diff --git a/pom.xml b/pom.xml index b688f3ee50..509dab4499 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3713-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 0033bd11d5..ef421d97ae 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3713-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index f62c8dc7f4..b56b9b8ccc 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3713-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 1f157e75bc..aa0a783459 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 3.3.0-SNAPSHOT + 3.3.0-GH-3713-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java index 15f10f7d6c..a97d64c52d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.aggregation; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -45,6 +46,19 @@ public static DateOperatorFactory dateOf(String fieldReference) { return new DateOperatorFactory(fieldReference); } + /** + * Take the date referenced by given {@literal fieldReference}. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link DateOperatorFactory}. + * @since 3.3 + */ + public static DateOperatorFactory zonedDateOf(String fieldReference, Timezone timezone) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new DateOperatorFactory(fieldReference).withTimezone(timezone); + } + /** * Take the date resulting from the given {@link AggregationExpression}. * @@ -57,6 +71,19 @@ public static DateOperatorFactory dateOf(AggregationExpression expression) { return new DateOperatorFactory(expression); } + /** + * Take the date resulting from the given {@link AggregationExpression}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link DateOperatorFactory}. + * @since 3.3 + */ + public static DateOperatorFactory zonedDateOf(AggregationExpression expression, Timezone timezone) { + + Assert.notNull(expression, "Expression must not be null!"); + return new DateOperatorFactory(expression).withTimezone(timezone); + } + /** * Take the given value as date. *

@@ -156,7 +183,7 @@ public static Timezone none() { * representing an Olson Timezone Identifier or UTC Offset. * * @param value the plain timezone {@link String}, a {@link Field} holding the timezone or an - * {@link AggregationExpression} resulting in the timezone. + * {@link AggregationExpression} resulting in the timezone. * @return new instance of {@link Timezone}. */ public static Timezone valueOf(Object value) { @@ -274,6 +301,41 @@ public DateOperatorFactory withTimezone(Timezone timezone) { return new DateOperatorFactory(fieldReference, expression, dateValue, timezone); } + /** + * Creates new {@link AggregationExpression} that adds the value of the given {@link AggregationExpression + * expression} (in {@literal units). @param expression must not be {@literal null}. + * + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateAdd}. + * @since 3.3 + */ + public DateAdd addValueOf(AggregationExpression expression, String unit) { + return applyTimezone(DateAdd.addValueOf(expression, unit).toDate(dateReference()), timezone); + } + + /** + * Creates new {@link AggregationExpression} that adds the value stored at the given {@literal field} (in + * {@literal units). @param fieldReference must not be {@literal null}. + * + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateAdd}. + * @since 3.3 + */ + public DateAdd addValueOf(String fieldReference, String unit) { + return applyTimezone(DateAdd.addValueOf(fieldReference, unit).toDate(dateReference()), timezone); + } + + /** + * Creates new {@link AggregationExpression} that adds the given value (in {@literal units). @param value must not + * be {@literal null}. @param unit the unit of measure. Must not be {@literal null}. + * + * @return + * @since 3.3 new instance of {@link DateAdd}. + */ + public DateAdd add(Object value, String unit) { + return applyTimezone(DateAdd.addValue(value, unit).toDate(dateReference()), timezone); + } + /** * Creates new {@link AggregationExpression} that returns the day of the year for a date as a number between 1 and * 366. @@ -304,6 +366,42 @@ public DayOfWeek dayOfWeek() { return applyTimezone(DayOfWeek.dayOfWeek(dateReference()), timezone); } + /** + * Creates new {@link AggregationExpression} that calculates the difference (in {@literal units) to the date + * computed by the given {@link AggregationExpression expression}. @param expression must not be {@literal null}. + * + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateAdd}. + * @since 3.3 + */ + public DateDiff diffValueOf(AggregationExpression expression, String unit) { + return applyTimezone(DateDiff.diffValueOf(expression, unit).toDate(dateReference()), timezone); + } + + /** + * Creates new {@link AggregationExpression} that calculates the difference (in {@literal units) to the date stored + * at the given {@literal field}. @param expression must not be {@literal null}. + * + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateAdd}. + * @since 3.3 + */ + public DateDiff diffValueOf(String fieldReference, String unit) { + return applyTimezone(DateDiff.diffValueOf(fieldReference, unit).toDate(dateReference()), timezone); + } + + /** + * Creates new {@link AggregationExpression} that calculates the difference (in {@literal units) to the date given + * {@literal value}. @param value anything the resolves to a valid date. Must not be {@literal null}. + * + * @param unit the unit of measure. Must not be {@literal null}. + * @return new instance of {@link DateAdd}. + * @since 3.3 + */ + public DateDiff diff(Object value, String unit) { + return applyTimezone(DateDiff.diffValue(value, unit).toDate(dateReference()), timezone); + } + /** * Creates new {@link AggregationExpression} that returns the year portion of a date. * @@ -1480,7 +1578,6 @@ protected java.util.Map append(String key, Object value) { } else { clone.put("timezone", ((Timezone) value).value); } - } else { clone.put(key, value); } @@ -1911,7 +2008,7 @@ default T millisecondOf(AggregationExpression expression) { * @author Matt Morrissette * @author Christoph Strobl * @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/ + * "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/ * @since 2.1 */ public static class DateFromParts extends TimezonedDateAggregationExpression implements DateParts { @@ -2086,7 +2183,7 @@ default DateFromParts yearOf(AggregationExpression expression) { * @author Matt Morrissette * @author Christoph Strobl * @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/ + * "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/ * @since 2.1 */ public static class IsoDateFromParts extends TimezonedDateAggregationExpression @@ -2262,7 +2359,7 @@ default IsoDateFromParts isoWeekYearOf(AggregationExpression expression) { * @author Matt Morrissette * @author Christoph Strobl * @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateToParts/ + * "https://docs.mongodb.com/manual/reference/operator/aggregation/dateToParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateToParts/ * @since 2.1 */ public static class DateToParts extends TimezonedDateAggregationExpression { @@ -2343,7 +2440,7 @@ protected String getMongoMethod() { * @author Matt Morrissette * @author Christoph Strobl * @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromString/ + * "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromString/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromString/ * @since 2.1 */ public static class DateFromString extends TimezonedDateAggregationExpression { @@ -2418,6 +2515,211 @@ protected String getMongoMethod() { } } + /** + * {@link AggregationExpression} for {@code $dateAdd}.
+ * NOTE: Requires MongoDB 5.0 or later. + * + * @author Christoph Strobl + * @since 3.3 + */ + public static class DateAdd extends TimezonedDateAggregationExpression { + + private DateAdd(Object value) { + super(value); + } + + /** + * Add the number of {@literal units} of the result of the given {@link AggregationExpression expression} to a + * {@link #toDate(Object) start date}. + * + * @param expression must not be {@literal null}. + * @param unit must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public static DateAdd addValueOf(AggregationExpression expression, String unit) { + return addValue(expression, unit); + } + + /** + * Add the number of {@literal units} from a {@literal field} to a {@link #toDate(Object) start date}. + * + * @param fieldReference must not be {@literal null}. + * @param unit must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public static DateAdd addValueOf(String fieldReference, String unit) { + return addValue(Fields.field(fieldReference), unit); + } + + /** + * Add the number of {@literal units} to a {@link #toDate(Object) start date}. + * + * @param value must not be {@literal null}. + * @param unit must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public static DateAdd addValue(Object value, String unit) { + + Map args = new HashMap<>(); + args.put("unit", unit); + args.put("amount", value); + return new DateAdd(args); + } + + /** + * Define the start date, in UTC, for the addition operation. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public DateAdd toDateOf(AggregationExpression expression) { + return toDate(expression); + } + + /** + * Define the start date, in UTC, for the addition operation. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public DateAdd toDateOf(String fieldReference) { + return toDate(Fields.field(fieldReference)); + } + + /** + * Define the start date, in UTC, for the addition operation. + * + * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public DateAdd toDate(Object dateExpression) { + return new DateAdd(append("startDate", dateExpression)); + } + + /** + * Optionally set the {@link Timezone} to use. If not specified {@literal UTC} is used. + * + * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. + * @return new instance of {@link DateAdd}. + */ + public DateAdd withTimezone(Timezone timezone) { + return new DateAdd(appendTimezone(argumentMap(), timezone)); + } + + @Override + protected String getMongoMethod() { + return "$dateAdd"; + } + } + + /** + * {@link AggregationExpression} for {@code $dateDiff}.
+ * NOTE: Requires MongoDB 5.0 or later. + * + * @author Christoph Strobl + * @since 3.3 + */ + public static class DateDiff extends TimezonedDateAggregationExpression { + + private DateDiff(Object value) { + super(value); + } + + /** + * Add the number of {@literal units} of the result of the given {@link AggregationExpression expression} to a + * {@link #toDate(Object) start date}. + * + * @param expression must not be {@literal null}. + * @param unit must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public static DateDiff diffValueOf(AggregationExpression expression, String unit) { + return diffValue(expression, unit); + } + + /** + * Add the number of {@literal units} from a {@literal field} to a {@link #toDate(Object) start date}. + * + * @param fieldReference must not be {@literal null}. + * @param unit must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public static DateDiff diffValueOf(String fieldReference, String unit) { + return diffValue(Fields.field(fieldReference), unit); + } + + /** + * Add the number of {@literal units} to a {@link #toDate(Object) start date}. + * + * @param value must not be {@literal null}. + * @param unit must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public static DateDiff diffValue(Object value, String unit) { + + Map args = new HashMap<>(); + args.put("unit", unit); + args.put("endDate", value); + return new DateDiff(args); + } + + /** + * Define the start date, in UTC, for the addition operation. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public DateDiff toDateOf(AggregationExpression expression) { + return toDate(expression); + } + + /** + * Define the start date, in UTC, for the addition operation. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public DateDiff toDateOf(String fieldReference) { + return toDate(Fields.field(fieldReference)); + } + + /** + * Define the start date, in UTC, for the addition operation. + * + * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}. + * @return new instance of {@link DateAdd}. + */ + public DateDiff toDate(Object dateExpression) { + return new DateDiff(append("startDate", dateExpression)); + } + + /** + * Optionally set the {@link Timezone} to use. If not specified {@literal UTC} is used. + * + * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. + * @return new instance of {@link DateAdd}. + */ + public DateDiff withTimezone(Timezone timezone) { + return new DateDiff(appendTimezone(argumentMap(), timezone)); + } + + /** + * Set the start day of the week if the unit if measure is set to {@literal week}. Uses {@literal Sunday} by + * default. + * + * @param day must not be {@literal null}. + * @return new instance of {@link DateDiff}. + */ + public DateDiff startOfWeek(Object day) { + return new DateDiff(append("startOfWeek", day)); + } + + @Override + protected String getMongoMethod() { + return "$dateDiff"; + } + } + @SuppressWarnings("unchecked") private static T applyTimezone(T instance, Timezone timezone) { return !ObjectUtils.nullSafeEquals(Timezone.none(), timezone) && !instance.hasTimezone() 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 5a2c48bc20..2eb1c8a1b3 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 @@ -130,6 +130,8 @@ public class MethodReferenceNode extends ExpressionNode { map.put("literal", singleArgRef().forOperator("$literal")); // DATE OPERATORS + map.put("dateAdd", mapArgRef().forOperator("$dateAdd").mappingParametersTo("startDate", "unit", "amount", "timezone")); + map.put("dateDiff", mapArgRef().forOperator("$dateDiff").mappingParametersTo("startDate", "endDate", "unit","timezone", "startOfWeek")); map.put("dayOfYear", singleArgRef().forOperator("$dayOfYear")); map.put("dayOfMonth", singleArgRef().forOperator("$dayOfMonth")); map.put("dayOfWeek", singleArgRef().forOperator("$dayOfWeek")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java new file mode 100644 index 0000000000..95f977ed73 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021. 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 + * + * https://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.jupiter.api.Test; +import org.springframework.data.mongodb.core.aggregation.DateOperators.Timezone; + +/** + * @author Christoph Strobl + */ +class DateOperatorsUnitTests { + + @Test // GH-3713 + void rendersDateAdd() { + + assertThat(DateOperators.dateOf("purchaseDate").add(3, "day").toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $dateAdd: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3 } }")); + } + + @Test // GH-3713 + void rendersDateAddWithTimezone() { + + assertThat(DateOperators.zonedDateOf("purchaseDate", Timezone.valueOf("America/Chicago")).add(3, "day") + .toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo(Document.parse( + "{ $dateAdd: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3, timezone : \"America/Chicago\" } }")); + } + + @Test // GH-3713 + void rendersDateDiff() { + + assertThat( + DateOperators.dateOf("purchaseDate").diffValueOf("delivered", "day").toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document + .parse("{ $dateDiff: { startDate: \"$purchaseDate\", endDate: \"$delivered\", unit: \"day\" } }")); + } + + @Test // GH-3713 + void rendersDateDiffWithTimezone() { + + assertThat(DateOperators.zonedDateOf("purchaseDate", Timezone.valueOf("America/Chicago")) + .diffValueOf("delivered", "day").toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo(Document.parse( + "{ $dateDiff: { startDate: \"$purchaseDate\", endDate: \"$delivered\", unit: \"day\", timezone : \"America/Chicago\" } }")); + } +} 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 b67beed126..532b58dce2 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 @@ -946,6 +946,16 @@ public void shouldRenderRoundWithPlace() { assertThat(transform("round(field, 2)")).isEqualTo(Document.parse("{ \"$round\" : [\"$field\", 2]}")); } + @Test // GH-3713 + void shouldRenderDateAdd() { + assertThat(transform("dateAdd(purchaseDate, 'day', 3)")).isEqualTo(Document.parse("{ $dateAdd: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3 } }")); + } + + @Test // GH-3713 + void shouldRenderDateDiff() { + assertThat(transform("dateDiff(purchaseDate, delivered, 'day')")).isEqualTo(Document.parse("{ $dateDiff: { startDate: \"$purchaseDate\", endDate: \"$delivered\", unit: \"day\" } }")); + } + 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);