From 4fcc09c6c17d1658ff92619a610badeb3919b4ff Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 6 Dec 2016 20:15:45 +0100 Subject: [PATCH] DATAMONGO-1548 - Add support for MongoDB 3.4 aggregation operators. We now support the following MongoDB 3.4 aggregation operators: $indexOfBytes, $indexOfCP, $split, $strLenBytes, $strLenCP, $substrCP, $indexOfArray, $range, $reverseArray, $reduce, $zip, $in, $isoDayOfWeek, $isoWeek, $isoWeekYear, $switch and $type. Original pull request: #423. --- .../aggregation/AggregationExpressions.java | 1483 ++++++++++++++++- .../core/aggregation/ExposedFields.java | 6 + .../data/mongodb/core/aggregation/Fields.java | 25 +- .../SpelExpressionTransformer.java | 6 +- .../core/spel/MethodReferenceNode.java | 18 + .../ProjectionOperationUnitTests.java | 310 ++++ .../SpelExpressionTransformerUnitTests.java | 174 +- src/main/asciidoc/reference/mongodb.adoc | 12 +- 8 files changed, 2002 insertions(+), 32 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java index c7b9fed3e..07150b8b2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java @@ -24,11 +24,15 @@ import java.util.List; import org.bson.Document; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Range; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Cond.OtherwiseBuilder; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Cond.ThenBuilder; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.AsBuilder; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let.ExpressionVariable; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Reduce.PropertyExpression; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Switch.CaseOperator; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -260,6 +264,30 @@ public interface AggregationExpressions { return IfNull.ifNull(expression); } + /** + * Creates new {@link AggregationExpression} that evaluates a series of {@link CaseOperator} expressions. When it + * finds an expression which evaluates to true, {@code $switch} executes a specified expression and breaks out of + * the control flow. + * + * @param conditions must not be {@literal null}. + * @return + */ + public static Switch switchCases(CaseOperator... conditions) { + return Switch.switchCases(conditions); + } + + /** + * Creates new {@link AggregationExpression} that evaluates a series of {@link CaseOperator} expressions. When it + * finds an expression which evaluates to true, {@code $switch} executes a specified expression and breaks out of + * the control flow. + * + * @param conditions must not be {@literal null}. + * @return + */ + public static Switch switchCases(List conditions) { + return Switch.switchCases(conditions); + } + public static class ConditionalOperatorFactory { private final String fieldReference; @@ -1562,6 +1590,184 @@ public interface AggregationExpressions { private StrCaseCmp createStrCaseCmp() { return fieldReference != null ? StrCaseCmp.valueOf(fieldReference) : StrCaseCmp.valueOf(expression); } + + /** + * Creates new {@link AggregationExpressions} that takes the associated string representation and searches a + * string for an occurence of a given {@literal substring} and returns the UTF-8 byte index (zero-based) of the + * first occurence. + * + * @param substring must not be {@literal null}. + * @return + */ + public IndexOfBytes indexOf(String substring) { + + Assert.notNull(substring, "Substring must not be null!"); + return createIndexOfBytesSubstringBuilder().indexOf(substring); + } + + /** + * Creates new {@link AggregationExpressions} that takes the associated string representation and searches a + * string for an occurence of a substring contained in the given {@literal field reference} and returns the UTF-8 + * byte index (zero-based) of the first occurence. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public IndexOfBytes indexOf(Field fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return createIndexOfBytesSubstringBuilder().indexOf(fieldReference); + } + + /** + * Creates new {@link AggregationExpressions} that takes the associated string representation and searches a + * string for an occurence of a substring resulting from the given {@link AggregationExpression} and returns the + * UTF-8 byte index (zero-based) of the first occurence. + * + * @param expression must not be {@literal null}. + * @return + */ + public IndexOfBytes indexOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return createIndexOfBytesSubstringBuilder().indexOf(expression); + } + + private IndexOfBytes.SubstringBuilder createIndexOfBytesSubstringBuilder() { + return fieldReference != null ? IndexOfBytes.valueOf(fieldReference) : IndexOfBytes.valueOf(expression); + } + + /** + * Creates new {@link AggregationExpressions} that takes the associated string representation and searches a + * string for an occurence of a given {@literal substring} and returns the UTF-8 code point index (zero-based) of + * the first occurence. + * + * @param substring must not be {@literal null}. + * @return + */ + public IndexOfCP indexOfCP(String substring) { + + Assert.notNull(substring, "Substring must not be null!"); + return createIndexOfCPSubstringBuilder().indexOf(substring); + } + + /** + * Creates new {@link AggregationExpressions} that takes the associated string representation and searches a + * string for an occurence of a substring contained in the given {@literal field reference} and returns the UTF-8 + * code point index (zero-based) of the first occurence. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public IndexOfCP indexOfCP(Field fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return createIndexOfCPSubstringBuilder().indexOf(fieldReference); + } + + /** + * Creates new {@link AggregationExpressions} that takes the associated string representation and searches a + * string for an occurence of a substring resulting from the given {@link AggregationExpression} and returns the + * UTF-8 code point index (zero-based) of the first occurence. + * + * @param expression must not be {@literal null}. + * @return + */ + public IndexOfCP indexOfCP(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return createIndexOfCPSubstringBuilder().indexOf(expression); + } + + private IndexOfCP.SubstringBuilder createIndexOfCPSubstringBuilder() { + return fieldReference != null ? IndexOfCP.valueOf(fieldReference) : IndexOfCP.valueOf(expression); + } + + /** + * Creates new {@link AggregationExpression} that divides the associated string representation into an array of + * substrings based on the given delimiter. + * + * @param delimiter must not be {@literal null}. + * @return + */ + public Split split(String delimiter) { + return createSplit().split(delimiter); + } + + /** + * Creates new {@link AggregationExpression} that divides the associated string representation into an array of + * substrings based on the delimiter resulting from the referenced field.. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public Split split(Field fieldReference) { + return createSplit().split(fieldReference); + } + + /** + * Creates new {@link AggregationExpression} that divides the associated string representation into an array of + * substrings based on a delimiter resulting from the given {@link AggregationExpression}. + * + * @param expression must not be {@literal null}. + * @return + */ + public Split split(AggregationExpression expression) { + return createSplit().split(expression); + } + + private Split createSplit() { + return fieldReference != null ? Split.valueOf(fieldReference) : Split.valueOf(expression); + } + + /** + * Creates new {@link AggregationExpression} that returns the number of UTF-8 bytes in the associated string + * representation. + * + * @return + */ + public StrLenBytes length() { + return fieldReference != null ? StrLenBytes.stringLengthOf(fieldReference) + : StrLenBytes.stringLengthOf(expression); + } + + /** + * Creates new {@link AggregationExpression} that returns the number of UTF-8 code points in the associated string + * representation. + * + * @return + */ + public StrLenCP lengthCP() { + return fieldReference != null ? StrLenCP.stringLengthOfCP(fieldReference) + : StrLenCP.stringLengthOfCP(expression); + } + + /** + * Creates new {@link AggregationExpressions} that takes the associated string representation and returns a + * substring starting at a specified code point index position. + * + * @param codePointStart + * @return + */ + public SubstrCP substringCP(int codePointStart) { + return substringCP(codePointStart, -1); + } + + /** + * Creates new {@link AggregationExpressions} that takes the associated string representation and returns a + * substring starting at a specified code point index position including the specified number of code points. + * + * @param codePointStart + * @param nrOfCodePoints + * @return + */ + public SubstrCP substringCP(int codePointStart, int nrOfCodePoints) { + return createSubstrCP().substringCP(codePointStart, nrOfCodePoints); + } + + private SubstrCP createSubstrCP() { + return fieldReference != null ? SubstrCP.valueOf(fieldReference) : SubstrCP.valueOf(expression); + } } } @@ -1729,6 +1935,89 @@ public interface AggregationExpressions { return usesFieldRef() ? Slice.sliceArrayOf(fieldReference) : Slice.sliceArrayOf(expression); } + /** + * Creates new {@link AggregationExpressions} that searches the associated array for an occurence of a specified + * value and returns the array index (zero-based) of the first occurence. + * + * @param value must not be {@literal null}. + * @return + */ + public IndexOfArray indexOf(Object value) { + return usesFieldRef() ? IndexOfArray.arrayOf(fieldReference).indexOf(value) + : IndexOfArray.arrayOf(expression).indexOf(value); + } + + /** + * Creates new {@link AggregationExpressions} that returns an array with the elements in reverse order. + * + * @return + */ + public ReverseArray reverse() { + return usesFieldRef() ? ReverseArray.reverseArrayOf(fieldReference) : ReverseArray.reverseArrayOf(expression); + } + + /** + * Start creating new {@link AggregationExpressions} that applies an {@link AggregationExpression} to each element + * in an array and combines them into a single value. + * + * @param expression must not be {@literal null}. + * @return + */ + public ReduceInitialValueBuilder reduce(final AggregationExpression expression) { + return new ReduceInitialValueBuilder() { + @Override + public Reduce startingWith(Object initialValue) { + return (usesFieldRef() ? Reduce.arrayOf(fieldReference) : Reduce.arrayOf(expression)) + .withInitialValue(initialValue).reduce(expression); + } + }; + } + + /** + * Start creating new {@link AggregationExpressions} that applies an {@link AggregationExpression} to each element + * in an array and combines them into a single value. + * + * @param expressions + * @return + */ + public ReduceInitialValueBuilder reduce(final PropertyExpression... expressions) { + + return new ReduceInitialValueBuilder() { + @Override + public Reduce startingWith(Object initialValue) { + return (usesFieldRef() ? Reduce.arrayOf(fieldReference) : Reduce.arrayOf(expression)) + .withInitialValue(initialValue).reduce(expressions); + } + }; + } + + /** + * Creates new {@link AggregationExpressions} that transposes an array of input arrays so that the first element + * of the output array would be an array containing, the first element of the first input array, the first element + * of the second input array, etc + * + * @param arrays must not be {@literal null}. + * @return + */ + public Zip zipWith(Object... arrays) { + return (usesFieldRef() ? Zip.arrayOf(fieldReference) : Zip.arrayOf(expression)).zip(arrays); + } + + /** + * Creates new {@link AggregationExpressions} that returns a boolean indicating whether a specified value is in + * the associcated array. + * + * @param value must not be {@literal null}. + * @return + */ + public In containsValue(Object value) { + return (usesFieldRef() ? In.arrayOf(fieldReference) : In.arrayOf(expression)).containsValue(value); + } + + public interface ReduceInitialValueBuilder { + Reduce startingWith(Object initialValue); + } + private boolean usesFieldRef() { return fieldReference != null; } @@ -1950,6 +2239,35 @@ public interface AggregationExpressions { .toString(format); } + /** + * Creates new {@link AggregationExpressions} that returns the weekday number in ISO 8601 format, ranging from 1 + * (for Monday) to 7 (for Sunday). + * + * @return + */ + public IsoDayOfWeek isoDayOfWeek() { + return usesFieldRef() ? IsoDayOfWeek.isoDayOfWeek(fieldReference) : IsoDayOfWeek.isoDayOfWeek(expression); + } + + /** + * Creates new {@link AggregationExpressions} that returns the week number in ISO 8601 format, ranging from 1 to + * 53. + * + * @return + */ + public IsoWeek isoWeek() { + return usesFieldRef() ? IsoWeek.isoWeekOf(fieldReference) : IsoWeek.isoWeekOf(expression); + } + + /** + * Creates new {@link AggregationExpressions} that returns the year number in ISO 8601 format. + * + * @return + */ + public IsoWeekYear isoWeekYear() { + return usesFieldRef() ? IsoWeekYear.isoWeekYearOf(fieldReference) : IsoWeekYear.isoWeekYearOf(expression); + } + private boolean usesFieldRef() { return fieldReference != null; } @@ -2070,6 +2388,17 @@ public interface AggregationExpressions { return context.getReference((Field) value).toString(); } + if (value instanceof List) { + + List sourceList = (List) value; + List mappedList = new ArrayList(sourceList.size()); + + for (Object item : sourceList) { + mappedList.add(unpack(item, context)); + } + return mappedList; + } + return value; } @@ -2092,17 +2421,29 @@ public interface AggregationExpressions { return Arrays.asList(this.value, value); } - protected Object append(String key, Object value) { + protected java.util.Map append(String key, Object value) { - if (!(value instanceof java.util.Map)) { + if (!(this.value instanceof java.util.Map)) { throw new IllegalArgumentException("o_O"); } - java.util.Map clone = new LinkedHashMap((java.util.Map) value); + java.util.Map clone = new LinkedHashMap( + (java.util.Map) this.value); clone.put(key, value); return clone; } + protected List values() { + + if (value instanceof List) { + return new ArrayList((List) value); + } + if (value instanceof java.util.Map) { + return new ArrayList(((java.util.Map) value).values()); + } + return new ArrayList(Arrays.asList(value)); + } + protected abstract String getMongoMethod(); } @@ -3440,6 +3781,10 @@ public interface AggregationExpressions { } } + // ######################################### + // STRING OPERATORS + // ######################################### + /** * {@link AggregationExpression} for {@code $concat}. * @@ -3733,6 +4078,342 @@ public interface AggregationExpressions { } } + /** + * {@link AggregationExpression} for {@code $indexOfBytes}. + * + * @author Christoph Strobl + */ + class IndexOfBytes extends AbstractAggregationExpression { + + private IndexOfBytes(List value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$indexOfBytes"; + } + + /** + * Start creating a new {@link IndexOfBytes}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static SubstringBuilder valueOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new SubstringBuilder(Fields.field(fieldReference)); + } + + /** + * Start creating a new {@link IndexOfBytes}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static SubstringBuilder valueOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new SubstringBuilder(expression); + } + + /** + * Optionally define the substring search start and end position. + * + * @param range must not be {@literal null}. + * @return + */ + public IndexOfBytes within(Range range) { + + Assert.notNull(range, "Range must not be null!"); + + List rangeValues = new ArrayList(2); + rangeValues.add(range.getLowerBound()); + if (range.getUpperBound() != null) { + rangeValues.add(range.getUpperBound()); + } + + return new IndexOfBytes(append(rangeValues)); + } + + public static class SubstringBuilder { + + private final Object stringExpression; + + private SubstringBuilder(Object stringExpression) { + this.stringExpression = stringExpression; + } + + public IndexOfBytes indexOf(String substring) { + return new IndexOfBytes(Arrays.asList(stringExpression, substring)); + } + + public IndexOfBytes indexOf(AggregationExpression expression) { + return new IndexOfBytes(Arrays.asList(stringExpression, expression)); + } + + public IndexOfBytes indexOf(Field fieldReference) { + return new IndexOfBytes(Arrays.asList(stringExpression, fieldReference)); + } + } + } + + /** + * {@link AggregationExpression} for {@code $indexOfCP}. + * + * @author Christoph Strobl + */ + class IndexOfCP extends AbstractAggregationExpression { + + private IndexOfCP(List value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$indexOfCP"; + } + + /** + * Start creating a new {@link IndexOfCP}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static SubstringBuilder valueOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new SubstringBuilder(Fields.field(fieldReference)); + } + + /** + * Start creating a new {@link IndexOfCP}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static SubstringBuilder valueOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new SubstringBuilder(expression); + } + + /** + * Optionally define the substring search start and end position. + * + * @param range must not be {@literal null}. + * @return + */ + public IndexOfCP within(Range range) { + + Assert.notNull(range, "Range must not be null!"); + + List rangeValues = new ArrayList(2); + rangeValues.add(range.getLowerBound()); + if (range.getUpperBound() != null) { + rangeValues.add(range.getUpperBound()); + } + + return new IndexOfCP(append(rangeValues)); + } + + public static class SubstringBuilder { + + private final Object stringExpression; + + private SubstringBuilder(Object stringExpression) { + this.stringExpression = stringExpression; + } + + public IndexOfCP indexOf(String substring) { + return new IndexOfCP(Arrays.asList(stringExpression, substring)); + } + + public IndexOfCP indexOf(AggregationExpression expression) { + return new IndexOfCP(Arrays.asList(stringExpression, expression)); + } + + public IndexOfCP indexOf(Field fieldReference) { + return new IndexOfCP(Arrays.asList(stringExpression, fieldReference)); + } + } + } + + /** + * {@link AggregationExpression} for {@code $split}. + */ + class Split extends AbstractAggregationExpression { + + private Split(List values) { + super(values); + } + + @Override + protected String getMongoMethod() { + return "$split"; + } + + /** + * Start creating a new {@link Split}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static Split valueOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new Split(asFields(fieldReference)); + } + + /** + * Start creating a new {@link Split}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static Split valueOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new Split(Collections.singletonList(expression)); + } + + /** + * Use given {@link String} as deliminator + * + * @param deliminator must not be {@literal null}. + * @return + */ + public Split split(String deliminator) { + + Assert.notNull(deliminator, "Deliminator must not be null!"); + return new Split(append(deliminator)); + } + + /** + * Usge value of referenced field as deliminator. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public Split split(Field fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new Split(append(fieldReference)); + } + + /** + * Use value resulting from {@link AggregationExpression} as deliminator. + * + * @param expression must not be {@literal null}. + * @return + */ + public Split split(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new Split(append(expression)); + } + } + + /** + * {@link AggregationExpression} for {@code $strLenBytes}. + */ + class StrLenBytes extends AbstractAggregationExpression { + + private StrLenBytes(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$strLenBytes"; + } + + public static StrLenBytes stringLengthOf(String fieldReference) { + return new StrLenBytes(Fields.field(fieldReference)); + } + + public static StrLenBytes stringLengthOf(AggregationExpression expression) { + return new StrLenBytes(expression); + } + } + + /** + * {@link AggregationExpression} for {@code $strLenCP}. + */ + class StrLenCP extends AbstractAggregationExpression { + + private StrLenCP(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$strLenCP"; + } + + public static StrLenCP stringLengthOfCP(String fieldReference) { + return new StrLenCP(Fields.field(fieldReference)); + } + + public static StrLenCP stringLengthOfCP(AggregationExpression expression) { + return new StrLenCP(expression); + } + } + + /** + * {@link AggregationExpression} for {@code $substrCP}. + * + * @author Christoph Strobl + */ + class SubstrCP extends AbstractAggregationExpression { + + private SubstrCP(List value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$substrCP"; + } + + /** + * Creates new {@link SubstrCP}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static SubstrCP valueOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new SubstrCP(asFields(fieldReference)); + } + + /** + * Creates new {@link SubstrCP}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static SubstrCP valueOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new SubstrCP(Collections.singletonList(expression)); + } + + public SubstrCP substringCP(int start) { + return substringCP(start, -1); + } + + public SubstrCP substringCP(int start, int nrOfChars) { + return new SubstrCP(append(Arrays.asList(start, nrOfChars))); + } + } + + // ######################################### + // ARRAY OPERATORS + // ######################################### + /** * {@link AggregationExpression} for {@code $arrayElementAt}. * @@ -4231,6 +4912,572 @@ public interface AggregationExpressions { } } + /** + * {@link AggregationExpression} for {@code $indexOfArray}. + * + * @author Christoph Strobl + */ + class IndexOfArray extends AbstractAggregationExpression { + + private IndexOfArray(List value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$indexOfArray"; + } + + /** + * Start creating new {@link IndexOfArray}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static IndexOfArrayBuilder arrayOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new IndexOfArrayBuilder(Fields.field(fieldReference)); + } + + /** + * Start creating new {@link IndexOfArray}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static IndexOfArrayBuilder arrayOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new IndexOfArrayBuilder(expression); + } + + public IndexOfArray within(Range range) { + + Assert.notNull(range, "Range must not be null!"); + + List rangeValues = new ArrayList(2); + rangeValues.add(range.getLowerBound()); + if (range.getUpperBound() != null) { + rangeValues.add(range.getUpperBound()); + } + + return new IndexOfArray(append(rangeValues)); + } + + public static class IndexOfArrayBuilder { + + private final Object targetArray; + + private IndexOfArrayBuilder(Object targetArray) { + this.targetArray = targetArray; + } + + public IndexOfArray indexOf(Object value) { + + Assert.notNull(value, "Value must not be null!"); + return new IndexOfArray(Arrays.asList(targetArray, value)); + } + } + } + + /** + * {@link AggregationExpression} for {@code $range}. + * + * @author Christoph Strobl + */ + class RangeOperator extends AbstractAggregationExpression { + + private RangeOperator(List values) { + super(values); + } + + @Override + protected String getMongoMethod() { + return "$range"; + } + + public static RangeOperatorBuilder rangeStartingAt(String fieldReference) { + return new RangeOperatorBuilder(Fields.field(fieldReference)); + } + + public static RangeOperatorBuilder rangeStartingAt(AggregationExpression expression) { + return new RangeOperatorBuilder(expression); + } + + public static RangeOperatorBuilder rangeStartingAt(Long value) { + return new RangeOperatorBuilder(value); + } + + public RangeOperator withStepSize(Long stepSize) { + return new RangeOperator(append(stepSize)); + } + + public static class RangeOperatorBuilder { + + private final Object startPoint; + + private RangeOperatorBuilder(Object startPoint) { + this.startPoint = startPoint; + } + + public RangeOperator to(Long index) { + return new RangeOperator(Arrays.asList(startPoint, index)); + } + + public RangeOperator to(AggregationExpression expression) { + return new RangeOperator(Arrays.asList(startPoint, expression)); + } + + public RangeOperator to(String fieldReference) { + return new RangeOperator(Arrays.asList(startPoint, Fields.field(fieldReference))); + } + } + + } + + /** + * {@link AggregationExpression} for {@code $reverseArray}. + */ + class ReverseArray extends AbstractAggregationExpression { + + private ReverseArray(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$reverseArray"; + } + + public static ReverseArray reverseArrayOf(String fieldReference) { + return new ReverseArray(Fields.field(fieldReference)); + } + + public static ReverseArray reverseArrayOf(AggregationExpression expression) { + return new ReverseArray(expression); + } + } + + /** + * {@link AggregationExpression} for {@code $reduce}. + */ + class Reduce implements AggregationExpression { + + private final Object input; + private final Object initialValue; + private final List reduceExpressions; + + private Reduce(Object input, Object initialValue, List reduceExpressions) { + this.input = input; + this.initialValue = initialValue; + this.reduceExpressions = reduceExpressions; + } + + @Override + public Document toDocument(AggregationOperationContext context) { + + Document document = new Document(); + + document.put("input", getMappedValue(input, context)); + document.put("initialValue", getMappedValue(initialValue, context)); + + if (reduceExpressions.iterator().next() instanceof PropertyExpression) { + + Document properties = new Document(); + for (AggregationExpression e : reduceExpressions) { + properties.putAll(e.toDocument(context)); + } + document.put("in", properties); + } else { + document.put("in", (reduceExpressions.iterator().next()).toDocument(context)); + } + + return new Document("$reduce", document); + } + + private Object getMappedValue(Object value, AggregationOperationContext context) { + + if (value instanceof Document) { + return value; + } + if (value instanceof AggregationExpression) { + return ((AggregationExpression) value).toDocument(context); + } else if (value instanceof Field) { + return context.getReference(((Field) value)).toString(); + } else { + return context.getMappedObject(new Document("###val###", value)).get("###val###"); + } + } + + public static InitialValueBuilder arrayOf(final String fieldReference) { + return new InitialValueBuilder() { + + @Override + public ReduceBuilder withInitialValue(final Object initialValue) { + return new ReduceBuilder() { + @Override + public Reduce reduce(AggregationExpression expression) { + return new Reduce(Fields.field(fieldReference), initialValue, Collections.singletonList(expression)); + } + + @Override + public Reduce reduce(PropertyExpression... expressions) { + return new Reduce(Fields.field(fieldReference), initialValue, + Arrays. asList(expressions)); + } + }; + } + }; + } + + public static InitialValueBuilder arrayOf(final AggregationExpression expression) { + return new InitialValueBuilder() { + + @Override + public ReduceBuilder withInitialValue(final Object initialValue) { + return new ReduceBuilder() { + @Override + public Reduce reduce(AggregationExpression expression) { + return new Reduce(expression, initialValue, Collections.singletonList(expression)); + } + + @Override + public Reduce reduce(PropertyExpression... expressions) { + return new Reduce(expression, initialValue, Arrays. asList(expressions)); + } + }; + } + }; + } + + public interface InitialValueBuilder { + + /** + * Define the initial cumulative value set before in is applied to the first element of the input array. + * + * @param intialValue must not be {@literal null}. + * @return + */ + ReduceBuilder withInitialValue(Object intialValue); + } + + public interface ReduceBuilder { + + /** + * Define the {@link AggregationExpression} to apply to each element in the input array in left-to-right order. + *
+ * NOTE: During evaulation of the in expression the variable references {@link Variable#THIS} and + * {@link Variable#VALUE} are availble. + * + * @param expression must not be {@literal null}. + * @return + */ + Reduce reduce(AggregationExpression expression); + + /** + * Define the {@link PropertyExpression}s to apply to each element in the input array in left-to-right order. + *
+ * NOTE: During evaulation of the in expression the variable references {@link Variable#THIS} and + * {@link Variable#VALUE} are availble. + * + * @param expression must not be {@literal null}. + * @return + */ + Reduce reduce(PropertyExpression... expressions); + } + + /** + * @author Christoph Strobl + */ + public static class PropertyExpression implements AggregationExpression { + + private final String propertyName; + private final AggregationExpression aggregationExpression; + + public PropertyExpression(String propertyName, AggregationExpression aggregationExpression) { + this.propertyName = propertyName; + this.aggregationExpression = aggregationExpression; + } + + /** + * Define a result property for an {@link AggregationExpression} used in {@link Reduce}. + * + * @param name must not be {@literal null}. + * @return + */ + public static AsBuilder property(final String name) { + return new AsBuilder() { + @Override + public PropertyExpression definedAs(AggregationExpression expression) { + return new PropertyExpression(name, expression); + } + }; + } + + @Override + public Document toDocument(AggregationOperationContext context) { + return new Document(propertyName, aggregationExpression.toDocument(context)); + } + + interface AsBuilder { + + /** + * Set the {@link AggregationExpression} resulting in the properties value. + * + * @param expression must not be {@literal null}. + * @return + */ + PropertyExpression definedAs(AggregationExpression expression); + } + } + + public enum Variable implements Field { + THIS { + @Override + public String getName() { + return "$$this"; + } + + @Override + public String getTarget() { + return "$$this"; + } + + @Override + public boolean isAliased() { + return false; + } + + @Override + public String toString() { + return getName(); + } + }, + VALUE { + @Override + public String getName() { + return "$$value"; + } + + @Override + public String getTarget() { + return "$$value"; + } + + @Override + public boolean isAliased() { + return false; + } + + @Override + public String toString() { + return getName(); + } + }; + + /** + * Create a {@link Field} reference to a given {@literal property} prefixed with the {@link Variable} identifier. + * eg. {@code $$value.product} + * + * @param property must not be {@literal null}. + * @return + */ + public Field referingTo(final String property) { + + return new Field() { + @Override + public String getName() { + return Variable.this.getName() + "." + property; + } + + @Override + public String getTarget() { + return Variable.this.getTarget() + "." + property; + } + + @Override + public boolean isAliased() { + return false; + } + + @Override + public String toString() { + return getName(); + } + }; + } + } + } + + /** + * {@link AggregationExpression} for {@code $zip}. + */ + class Zip extends AbstractAggregationExpression { + + protected Zip(java.util.Map value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$zip"; + } + + /** + * Start creating new {@link Zip}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static ZipBuilder arrayOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new ZipBuilder(Fields.field(fieldReference)); + } + + /** + * Start creating new {@link Zip}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static ZipBuilder arrayOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new ZipBuilder(expression); + } + + /** + * Create new {@link Zip} and set the {@code useLongestLength} property to {@literal true}. + * + * @return + */ + public Zip useLongestLength() { + return new Zip(append("useLongestLength", true)); + } + + /** + * Optionally provide a default value. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public Zip defaultTo(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new Zip(append("defaults", Fields.field(fieldReference))); + } + + /** + * Optionally provide a default value. + * + * @param expression must not be {@literal null}. + * @return + */ + public Zip defaultTo(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new Zip(append("defaults", expression)); + } + + /** + * Optionally provide a default value. + * + * @param array must not be {@literal null}. + * @return + */ + public Zip defaultTo(Object[] array) { + + Assert.notNull(array, "Array must not be null!"); + return new Zip(append("defaults", Arrays.asList(array))); + } + + public static class ZipBuilder { + + private final List sourceArrays; + + public ZipBuilder(Object sourceArray) { + + this.sourceArrays = new ArrayList(); + this.sourceArrays.add(sourceArray); + } + + /** + * Creates new {@link Zip} that transposes an array of input arrays so that the first element of the output array + * would be an array containing, the first element of the first input array, the first element of the second input + * array, etc + * + * @param arrays arrays to zip the referenced one with. must not be {@literal null}. + * @return + */ + public Zip zip(Object... arrays) { + + Assert.notNull(arrays, "Arrays must not be null!"); + for (Object value : arrays) { + + if (value instanceof String) { + sourceArrays.add(Fields.field((String) value)); + } else { + sourceArrays.add(value); + } + } + + return new Zip(Collections. singletonMap("inputs", sourceArrays)); + } + } + } + + /** + * {@link AggregationExpression} for {@code $in}. + */ + class In extends AbstractAggregationExpression { + + private In(List values) { + super(values); + } + + @Override + protected String getMongoMethod() { + return "$in"; + } + + public static InBuilder arrayOf(final String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new InBuilder() { + @Override + public In containsValue(Object value) { + + Assert.notNull(value, "Value must not be null!"); + return new In(Arrays.asList(value, Fields.field(fieldReference))); + } + }; + } + + public static InBuilder arrayOf(final AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new InBuilder() { + @Override + public In containsValue(Object value) { + + Assert.notNull(value, "Value must not be null!"); + return new In(Arrays.asList(value, expression)); + } + }; + } + + public interface InBuilder { + In containsValue(Object value); + } + + } + + // ############ + // LITERAL OPERATORS + // ############ + /** * {@link AggregationExpression} for {@code $literal}. * @@ -4745,6 +5992,129 @@ public interface AggregationExpressions { } } + /** + * {@link AggregationExpression} for {@code $isoDayOfWeek}. + * + * @author Christoph Strobl + */ + class IsoDayOfWeek extends AbstractAggregationExpression { + + private IsoDayOfWeek(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$isoDayOfWeek"; + } + + /** + * Creates new {@link IsoDayOfWeek}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static IsoDayOfWeek isoDayOfWeek(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new IsoDayOfWeek(Fields.field(fieldReference)); + } + + /** + * Creates new {@link IsoDayOfWeek}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static IsoDayOfWeek isoDayOfWeek(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new IsoDayOfWeek(expression); + } + } + + /** + * {@link AggregationExpression} for {@code $isoWeek}. + * + * @author Christoph Strobl + */ + class IsoWeek extends AbstractAggregationExpression { + + private IsoWeek(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$isoWeek"; + } + + /** + * Creates new {@link IsoWeek}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static IsoWeek isoWeekOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new IsoWeek(Fields.field(fieldReference)); + } + + /** + * Creates new {@link IsoWeek}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static IsoWeek isoWeekOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new IsoWeek(expression); + } + } + + /** + * {@link AggregationExpression} for {@code $isoWeekYear}. + * + * @author Christoph Strobl + */ + class IsoWeekYear extends AbstractAggregationExpression { + + private IsoWeekYear(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$isoWeekYear"; + } + + /** + * Creates new {@link IsoWeekYear}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static IsoWeekYear isoWeekYearOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new IsoWeekYear(Fields.field(fieldReference)); + } + + /** + * Creates new {@link Millisecond}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static IsoWeekYear isoWeekYearOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new IsoWeekYear(expression); + } + } + /** * {@link AggregationExpression} for {@code $sum}. * @@ -6863,4 +8233,111 @@ public interface AggregationExpressions { } } } + + + /** + * {@link AggregationExpression} for {@code $switch}. + * + * @author Christoph Strobl + */ + class Switch extends AbstractAggregationExpression { + + private Switch(java.util.Map values) { + super(values); + } + + @Override + protected String getMongoMethod() { + return "$switch"; + } + + public static Switch switchCases(CaseOperator... conditions) { + + Assert.notNull(conditions, "Conditions must not be null!"); + return switchCases(Arrays.asList(conditions)); + } + + public static Switch switchCases(List conditions) { + + Assert.notNull(conditions, "Conditions must not be null!"); + return new Switch(Collections. singletonMap("branches", new ArrayList(conditions))); + } + + public Switch defaultTo(Object value) { + return new Switch(append("default", value)); + } + + public static class CaseOperator implements AggregationExpression { + + private final AggregationExpression when; + private final Object then; + + private CaseOperator(AggregationExpression when, Object then) { + + this.when = when; + this.then = then; + } + + public static ThenBuilder when(final AggregationExpression condition) { + + Assert.notNull(condition, "Condition must not be null!"); + return new ThenBuilder() { + @Override + public CaseOperator then(Object value) { + + Assert.notNull(value, "Value must not be null!"); + return new CaseOperator(condition, value); + } + }; + } + + @Override + public Document toDocument(AggregationOperationContext context) { + Document document = new Document("case", when.toDocument(context)); + + if (then instanceof AggregationExpression) { + document.put("then", ((AggregationExpression) then).toDocument(context)); + } else if (then instanceof Field) { + document.put("then", context.getReference((Field) then).toString()); + } else { + document.put("then", then); + } + + return document; + } + + public interface ThenBuilder { + CaseOperator then(Object value); + } + } + } + + /** + * {@link AggregationExpression} for {@code $type}. + * + * @author Christoph Strobl + */ + class Type extends AbstractAggregationExpression { + + private Type(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$type"; + } + + /** + * Creates new {@link Type}. + * + * @param field must not be {@literal null}. + * @return + */ + public static Type typeOf(String field) { + + Assert.notNull(field, "Field must not be null!"); + return new Type(Fields.field(field)); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java index f9033743d..ce51f4062 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java @@ -22,6 +22,7 @@ import java.util.Iterator; import java.util.List; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField; import org.springframework.util.Assert; import org.springframework.util.CompositeIterator; import org.springframework.util.ObjectUtils; @@ -406,6 +407,11 @@ public final class ExposedFields implements Iterable { */ @Override public String toString() { + + if(getRaw().startsWith("$")) { + return getRaw(); + } + return String.format("$%s", getRaw()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java index 183d52652..2ba33412a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java @@ -187,13 +187,13 @@ public final class Fields implements Iterable { } /** - * * @return * @since 1.10 */ public List asList() { return Collections.unmodifiableList(fields); } + /** * Value object to encapsulate a field in an aggregation operation. * @@ -201,6 +201,7 @@ public final class Fields implements Iterable { */ static class AggregationField implements Field { + private final String raw; private final String name; private final String target; @@ -225,6 +226,7 @@ public final class Fields implements Iterable { */ public AggregationField(String name, String target) { + raw = name; String nameToSet = cleanUp(name); String targetToSet = cleanUp(target); @@ -266,6 +268,11 @@ public final class Fields implements Iterable { * @see org.springframework.data.mongodb.core.aggregation.Field#getAlias() */ public String getTarget() { + + if (isLocalVar()) { + return this.getRaw(); + } + return StringUtils.hasText(this.target) ? this.target : this.name; } @@ -278,6 +285,22 @@ public final class Fields implements Iterable { return !getName().equals(getTarget()); } + /** + * @return {@literal true} in case the field name starts with {@code $$}. + * @since 1.10 + */ + public boolean isLocalVar() { + return raw.startsWith("$$") && !raw.startsWith("$$$"); + } + + /** + * @return + * @since 1.10 + */ + public String getRaw() { + return raw; + } + /* * (non-Javadoc) * @see java.lang.Object#toString() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java index 494accf28..96aaaf872 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java @@ -487,8 +487,10 @@ class SpelExpressionTransformer implements AggregationExpressionTransformer { } else if (ObjectUtils.nullSafeEquals(methodReference.getArgumentType(), ArgumentType.MAP)) { Document dbo = new Document(); - for (int i = 0; i < methodReference.getArgumentMap().length; i++) { - dbo.put(methodReference.getArgumentMap()[i], transform(node.getChild(i), context)); + + int i = 0; + for(ExpressionNode child : node) { + dbo.put(methodReference.getArgumentMap()[i++], transform(child, context)); } args = dbo; } else { 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 18ffc5a44..fcdd5c15c 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 @@ -90,6 +90,12 @@ public class MethodReferenceNode extends ExpressionNode { map.put("toLower", singleArgumentAggregationMethodReference().forOperator("$toLower")); map.put("toUpper", singleArgumentAggregationMethodReference().forOperator("$toUpper")); map.put("strcasecmp", arrayArgumentAggregationMethodReference().forOperator("$strcasecmp")); + map.put("indexOfBytes", arrayArgumentAggregationMethodReference().forOperator("$indexOfBytes")); + map.put("indexOfCP", arrayArgumentAggregationMethodReference().forOperator("$indexOfCP")); + map.put("split", arrayArgumentAggregationMethodReference().forOperator("$split")); + map.put("strLenBytes", singleArgumentAggregationMethodReference().forOperator("$strLenBytes")); + map.put("strLenCP", singleArgumentAggregationMethodReference().forOperator("$strLenCP")); + map.put("substrCP", arrayArgumentAggregationMethodReference().forOperator("$substrCP")); // TEXT SEARCH OPERATORS map.put("meta", singleArgumentAggregationMethodReference().forOperator("$meta")); @@ -102,6 +108,12 @@ public class MethodReferenceNode extends ExpressionNode { map.put("isArray", singleArgumentAggregationMethodReference().forOperator("$isArray")); map.put("size", singleArgumentAggregationMethodReference().forOperator("$size")); map.put("slice", arrayArgumentAggregationMethodReference().forOperator("$slice")); + map.put("reverseArray", singleArgumentAggregationMethodReference().forOperator("$reverseArray")); + map.put("reduce", mapArgumentAggregationMethodReference().forOperator("$reduce").mappingParametersTo("input", + "initialValue", "in")); + map.put("zip", mapArgumentAggregationMethodReference().forOperator("$zip").mappingParametersTo("inputs", + "useLongestLength", "defaults")); + map.put("in", arrayArgumentAggregationMethodReference().forOperator("$in")); // VARIABLE OPERATORS map.put("map", mapArgumentAggregationMethodReference().forOperator("$map") // @@ -124,6 +136,9 @@ public class MethodReferenceNode extends ExpressionNode { map.put("millisecond", singleArgumentAggregationMethodReference().forOperator("$millisecond")); map.put("dateToString", mapArgumentAggregationMethodReference().forOperator("$dateToString") // .mappingParametersTo("format", "date")); + map.put("isoDayOfWeek", singleArgumentAggregationMethodReference().forOperator("$isoDayOfWeek")); + map.put("isoWeek", singleArgumentAggregationMethodReference().forOperator("$isoWeek")); + map.put("isoWeekYear", singleArgumentAggregationMethodReference().forOperator("$isoWeekYear")); // CONDITIONAL OPERATORS map.put("cond", mapArgumentAggregationMethodReference().forOperator("$cond") // @@ -142,6 +157,9 @@ public class MethodReferenceNode extends ExpressionNode { map.put("stdDevPop", arrayArgumentAggregationMethodReference().forOperator("$stdDevPop")); map.put("stdDevSamp", arrayArgumentAggregationMethodReference().forOperator("$stdDevSamp")); + // TYPE OPERATORS + map.put("type", singleArgumentAggregationMethodReference().forOperator("$type")); + FUNCTIONS = Collections.unmodifiableMap(map); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java index 655c9397e..675f89a72 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java @@ -28,9 +28,30 @@ import java.util.Arrays; import java.util.List; import org.bson.Document; +import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.data.domain.Range; import org.springframework.data.mongodb.core.DocumentTestUtils; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.And; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ArithmeticOperators; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ArrayOperators; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Avg; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.BooleanOperators; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ComparisonOperators; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ConditionalOperators; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.DateOperators; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Gte; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Let.ExpressionVariable; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.LiteralOperators; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Lt; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.RangeOperator; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Reduce.PropertyExpression; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Reduce.Variable; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.SetOperators; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.StringOperators; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Switch.CaseOperator; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Type; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.VariableOperators; import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder; import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.*; @@ -1797,7 +1818,296 @@ public class ProjectionOperationUnitTests { "}}}}"))); } + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIndexOfBytesCorrectly() { + + Document agg = project().and(StringOperators.valueOf("item").indexOf("foo")).as("byteLocation") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project: { byteLocation: { $indexOfBytes: [ \"$item\", \"foo\" ] } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIndexOfBytesWithRangeCorrectly() { + + Document agg = project().and(StringOperators.valueOf("item").indexOf("foo").within(new Range(5L, 9L))) + .as("byteLocation").toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, isBsonObject().containing("$project.byteLocation.$indexOfBytes.[2]", 5L) + .containing("$project.byteLocation.$indexOfBytes.[3]", 9L)); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIndexOfCPCorrectly() { + + Document agg = project().and(StringOperators.valueOf("item").indexOfCP("foo")).as("cpLocation") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project: { cpLocation: { $indexOfCP: [ \"$item\", \"foo\" ] } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIndexOfCPWithRangeCorrectly() { + + Document agg = project().and(StringOperators.valueOf("item").indexOfCP("foo").within(new Range(5L, 9L))) + .as("cpLocation").toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, isBsonObject().containing("$project.cpLocation.$indexOfCP.[2]", 5L) + .containing("$project.cpLocation.$indexOfCP.[3]", 9L)); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderSplitCorrectly() { + + Document agg = project().and(StringOperators.valueOf("city").split(", ")).as("city_state") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { city_state : { $split: [\"$city\", \", \"] }} }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderStrLenBytesCorrectly() { + + Document agg = project().and(StringOperators.valueOf("name").length()).as("length") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { \"length\": { $strLenBytes: \"$name\" } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderStrLenCPCorrectly() { + + Document agg = project().and(StringOperators.valueOf("name").lengthCP()).as("length") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { \"length\": { $strLenCP: \"$name\" } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderSubstrCPCorrectly() { + + Document agg = project().and(StringOperators.valueOf("quarter").substringCP(0, 2)).as("yearSubstring") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { yearSubstring: { $substrCP: [ \"$quarter\", 0, 2 ] } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIndexOfArrayCorrectly() { + + Document agg = project().and(ArrayOperators.arrayOf("items").indexOf(2)).as("index") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { index: { $indexOfArray: [ \"$items\", 2 ] } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderRangeCorrectly() { + + Document agg = project().and(RangeOperator.rangeStartingAt(0L).to("distance").withStepSize(25L)).as("rest_stops") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, isBsonObject().containing("$project.rest_stops.$range.[0]", 0L) + .containing("$project.rest_stops.$range.[1]", "$distance").containing("$project.rest_stops.$range.[2]", 25L)); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderReverseArrayCorrectly() { + + Document agg = project().and(ArrayOperators.arrayOf("favorites").reverse()).as("reverseFavorites") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { reverseFavorites: { $reverseArray: \"$favorites\" } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderReduceWithSimpleObjectCorrectly() { + + Document agg = project() + .and(ArrayOperators.arrayOf("probabilityArr") + .reduce(ArithmeticOperators.valueOf("$$value").multiplyBy("$$this")).startingWith(1)) + .as("results").toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse( + "{ $project : { \"results\": { $reduce: { input: \"$probabilityArr\", initialValue: 1, in: { $multiply: [ \"$$value\", \"$$this\" ] } } } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderReduceWithComplexObjectCorrectly() { + + PropertyExpression sum = PropertyExpression.property("sum").definedAs( + ArithmeticOperators.valueOf(Variable.VALUE.referingTo("sum").getName()).add(Variable.THIS.getName())); + PropertyExpression product = PropertyExpression.property("product").definedAs(ArithmeticOperators + .valueOf(Variable.VALUE.referingTo("product").getName()).multiplyBy(Variable.THIS.getName())); + + Document agg = project() + .and(ArrayOperators.arrayOf("probabilityArr").reduce(sum, product) + .startingWith(new Document().append("sum", 5).append("product", 2))) + .as("results").toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse( + "{ $project : { \"results\": { $reduce: { input: \"$probabilityArr\", initialValue: { \"sum\" : 5 , \"product\" : 2} , in: { \"sum\": { $add : [\"$$value.sum\", \"$$this\"] }, \"product\": { $multiply: [ \"$$value.product\", \"$$this\" ] } } } } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderZipCorrectly() { + + AggregationExpression elemAt0 = ArrayOperators.arrayOf("matrix").elementAt(0); + AggregationExpression elemAt1 = ArrayOperators.arrayOf("matrix").elementAt(1); + AggregationExpression elemAt2 = ArrayOperators.arrayOf("matrix").elementAt(2); + + Document agg = project().and( + ArrayOperators.arrayOf(elemAt0).zipWith(elemAt1, elemAt2).useLongestLength().defaultTo(new Object[] { 1, 2 })) + .as("transposed").toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse( + "{ $project : { transposed: { $zip: { inputs: [ { $arrayElemAt: [ \"$matrix\", 0 ] }, { $arrayElemAt: [ \"$matrix\", 1 ] }, { $arrayElemAt: [ \"$matrix\", 2 ] } ], useLongestLength : true, defaults: [1,2] } } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderInCorrectly() { + + Document agg = project().and(ArrayOperators.arrayOf("in_stock").containsValue("bananas")).as("has_bananas") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { has_bananas : { $in : [\"bananas\", \"$in_stock\" ] } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIsoDayOfWeekCorrectly() { + + Document agg = project().and(DateOperators.dateOf("birthday").isoDayOfWeek()).as("dayOfWeek") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { dayOfWeek: { $isoDayOfWeek: \"$birthday\" } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIsoWeekCorrectly() { + + Document agg = project().and(DateOperators.dateOf("date").isoWeek()).as("weekNumber") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { weekNumber: { $isoWeek: \"$date\" } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIsoWeekYearCorrectly() { + + Document agg = project().and(DateOperators.dateOf("date").isoWeekYear()).as("yearNumber") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { yearNumber: { $isoWeekYear: \"$date\" } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderSwitchCorrectly() { + + String expected = "$switch:\n" + // + "{\n" + // + " branches: [\n" + // + " {\n" + // + " case: { $gte : [ { $avg : \"$scores\" }, 90 ] },\n" + // + " then: \"Doing great!\"\n" + // + " },\n" + // + " {\n" + // + " case: { $and : [ { $gte : [ { $avg : \"$scores\" }, 80 ] },\n" + // + " { $lt : [ { $avg : \"$scores\" }, 90 ] } ] },\n" + // + " then: \"Doing pretty well.\"\n" + // + " },\n" + // + " {\n" + // + " case: { $lt : [ { $avg : \"$scores\" }, 80 ] },\n" + // + " then: \"Needs improvement.\"\n" + // + " }\n" + // + " ],\n" + // + " default: \"No scores found.\"\n" + // + " }\n" + // + "}"; + + CaseOperator cond1 = CaseOperator.when(Gte.valueOf(Avg.avgOf("scores")).greaterThanEqualToValue(90)) + .then("Doing great!"); + CaseOperator cond2 = CaseOperator.when(And.and(Gte.valueOf(Avg.avgOf("scores")).greaterThanEqualToValue(80), + Lt.valueOf(Avg.avgOf("scores")).lessThanValue(90))).then("Doing pretty well."); + CaseOperator cond3 = CaseOperator.when(Lt.valueOf(Avg.avgOf("scores")).lessThanValue(80)) + .then("Needs improvement."); + + Document agg = project().and(ConditionalOperators.switchCases(cond1, cond2, cond3).defaultTo("No scores found.")) + .as("summary").toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { summary: {" + expected + "} } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldTypeCorrectly() { + + Document agg = project().and(Type.typeOf("a")).as("a") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(Document.parse("{ $project : { a: { $type: \"$a\" } } }"))); + } + private static Document exctractOperation(String field, Document fromProjectClause) { return (Document) fromProjectClause.get(field); } + } 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 b7c8e51dd..187318f27 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 @@ -249,7 +249,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderMethodReferenceNodeSetEqualsMixedArrays() { - assertThat(transform("setEquals(a, new int[]{4,5,6})"), is((Object) Document.parse("{ \"$setEquals\" : [ \"$a\" , [ 4 , 5 , 6]]}"))); + assertThat(transform("setEquals(a, new int[]{4,5,6})"), + is((Object) Document.parse("{ \"$setEquals\" : [ \"$a\" , [ 4 , 5 , 6]]}"))); } /** @@ -266,7 +267,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderMethodReferenceSetUnion() { - assertThat(transform("setUnion(a, new int[]{4,5,6})"), is((Object) Document.parse("{ \"$setUnion\" : [ \"$a\" , [ 4 , 5 , 6]]}"))); + assertThat(transform("setUnion(a, new int[]{4,5,6})"), + is((Object) Document.parse("{ \"$setUnion\" : [ \"$a\" , [ 4 , 5 , 6]]}"))); } /** @@ -274,7 +276,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderMethodReferenceSeDifference() { - assertThat(transform("setDifference(a, new int[]{4,5,6})"), is((Object) Document.parse("{ \"$setDifference\" : [ \"$a\" , [ 4 , 5 , 6]]}"))); + assertThat(transform("setDifference(a, new int[]{4,5,6})"), + is((Object) Document.parse("{ \"$setDifference\" : [ \"$a\" , [ 4 , 5 , 6]]}"))); } /** @@ -282,7 +285,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderMethodReferenceSetIsSubset() { - assertThat(transform("setIsSubset(a, new int[]{4,5,6})"), is((Object) Document.parse("{ \"$setIsSubset\" : [ \"$a\" , [ 4 , 5 , 6]]}"))); + assertThat(transform("setIsSubset(a, new int[]{4,5,6})"), + is((Object) Document.parse("{ \"$setIsSubset\" : [ \"$a\" , [ 4 , 5 , 6]]}"))); } /** @@ -483,7 +487,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderMethodReferenceNodeConcat() { - assertThat(transform("concat(a, b, 'c')"), is((Object) Document.parse("{ \"$concat\" : [ \"$a\" , \"$b\" , \"c\"]}"))); + assertThat(transform("concat(a, b, 'c')"), + is((Object) Document.parse("{ \"$concat\" : [ \"$a\" , \"$b\" , \"c\"]}"))); } /** @@ -539,7 +544,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderMethodReferenceNodeConcatArrays() { - assertThat(transform("concatArrays(a, b, c)"), is((Object) Document.parse("{ \"$concatArrays\" : [ \"$a\" , \"$b\" , \"$c\"]}"))); + assertThat(transform("concatArrays(a, b, c)"), + is((Object) Document.parse("{ \"$concatArrays\" : [ \"$a\" , \"$b\" , \"$c\"]}"))); } /** @@ -547,8 +553,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderMethodReferenceNodeFilter() { - assertThat(transform("filter(a, 'num', '$$num' > 10)"), - is((Object) Document.parse("{ \"$filter\" : { \"input\" : \"$a\" , \"as\" : \"num\" , \"cond\" : { \"$gt\" : [ \"$$num\" , 10]}}}"))); + assertThat(transform("filter(a, 'num', '$$num' > 10)"), is((Object) Document.parse( + "{ \"$filter\" : { \"input\" : \"$a\" , \"as\" : \"num\" , \"cond\" : { \"$gt\" : [ \"$$num\" , 10]}}}"))); } /** @@ -580,8 +586,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderMethodReferenceNodeMap() { - assertThat(transform("map(quizzes, 'grade', '$$grade' + 2)"), is( - (Object) Document.parse("{ \"$map\" : { \"input\" : \"$quizzes\" , \"as\" : \"grade\" , \"in\" : { \"$add\" : [ \"$$grade\" , 2]}}}"))); + assertThat(transform("map(quizzes, 'grade', '$$grade' + 2)"), is((Object) Document.parse( + "{ \"$map\" : { \"input\" : \"$quizzes\" , \"as\" : \"grade\" , \"in\" : { \"$add\" : [ \"$$grade\" , 2]}}}"))); } /** @@ -589,8 +595,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderMethodReferenceNodeLet() { - assertThat(transform("let({low:1, high:'$$low'}, gt('$$low', '$$high'))"), is( - (Object) Document.parse("{ \"$let\" : { \"vars\" : { \"low\" : 1 , \"high\" : \"$$low\"} , \"in\" : { \"$gt\" : [ \"$$low\" , \"$$high\"]}}}"))); + assertThat(transform("let({low:1, high:'$$low'}, gt('$$low', '$$high'))"), is((Object) Document.parse( + "{ \"$let\" : { \"vars\" : { \"low\" : 1 , \"high\" : \"$$low\"} , \"in\" : { \"$gt\" : [ \"$$low\" , \"$$high\"]}}}"))); } /** @@ -695,8 +701,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderMethodReferenceCond() { - assertThat(transform("cond(qty > 250, 30, 20)"), - is((Object) Document.parse("{ \"$cond\" : { \"if\" : { \"$gt\" : [ \"$qty\" , 250]} , \"then\" : 30 , \"else\" : 20}}"))); + assertThat(transform("cond(qty > 250, 30, 20)"), is((Object) Document + .parse("{ \"$cond\" : { \"if\" : { \"$gt\" : [ \"$qty\" , 250]} , \"then\" : 30 , \"else\" : 20}}"))); } /** @@ -755,13 +761,13 @@ public class SpelExpressionTransformerUnitTests { assertThat(transform("min(a, b)"), is((Object) Document.parse("{ \"$min\" : [ \"$a\" , \"$b\"]}"))); } - /** * @see DATAMONGO-1530 */ @Test public void shouldRenderMethodReferenceNodePush() { - assertThat(transform("push({'item':'$item', 'quantity':'$qty'})"), is((Object) Document.parse("{ \"$push\" : { \"item\" : \"$item\" , \"quantity\" : \"$qty\"}}"))); + assertThat(transform("push({'item':'$item', 'quantity':'$qty'})"), + is((Object) Document.parse("{ \"$push\" : { \"item\" : \"$item\" , \"quantity\" : \"$qty\"}}"))); } /** @@ -777,7 +783,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderMethodReferenceNodeStdDevPop() { - assertThat(transform("stdDevPop(scores.score)"), is((Object) Document.parse("{ \"$stdDevPop\" : [ \"$scores.score\"]}"))); + assertThat(transform("stdDevPop(scores.score)"), + is((Object) Document.parse("{ \"$stdDevPop\" : [ \"$scores.score\"]}"))); } /** @@ -857,8 +864,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderComplexOperationNodeOr() { - assertThat(transform("1+2 || concat(a, b) || true"), - is((Object) Document.parse("{ \"$or\" : [ { \"$add\" : [ 1 , 2]} , { \"$concat\" : [ \"$a\" , \"$b\"]} , true]}"))); + assertThat(transform("1+2 || concat(a, b) || true"), is((Object) Document + .parse("{ \"$or\" : [ { \"$add\" : [ 1 , 2]} , { \"$concat\" : [ \"$a\" , \"$b\"]} , true]}"))); } /** @@ -874,8 +881,8 @@ public class SpelExpressionTransformerUnitTests { */ @Test public void shouldRenderComplexOperationNodeAnd() { - assertThat(transform("1+2 && concat(a, b) && true"), - is((Object) Document.parse("{ \"$and\" : [ { \"$add\" : [ 1 , 2]} , { \"$concat\" : [ \"$a\" , \"$b\"]} , true]}"))); + assertThat(transform("1+2 && concat(a, b) && true"), is((Object) Document + .parse("{ \"$and\" : [ { \"$add\" : [ 1 , 2]} , { \"$concat\" : [ \"$a\" , \"$b\"]} , true]}"))); } /** @@ -894,6 +901,131 @@ public class SpelExpressionTransformerUnitTests { assertThat(transform("!(foo > 10)"), is((Object) Document.parse("{ \"$not\" : [ { \"$gt\" : [ \"$foo\" , 10]}]}"))); } + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceIndexOfBytes() { + assertThat(transform("indexOfBytes(item, 'foo')"), + is(Document.parse("{ \"$indexOfBytes\" : [ \"$item\" , \"foo\"]}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceIndexOfCP() { + assertThat(transform("indexOfCP(item, 'foo')"), is(Document.parse("{ \"$indexOfCP\" : [ \"$item\" , \"foo\"]}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceSplit() { + assertThat(transform("split(item, ',')"), is(Document.parse("{ \"$split\" : [ \"$item\" , \",\"]}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceStrLenBytes() { + assertThat(transform("strLenBytes(item)"), is(Document.parse("{ \"$strLenBytes\" : \"$item\"}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceStrLenCP() { + assertThat(transform("strLenCP(item)"), is(Document.parse("{ \"$strLenCP\" : \"$item\"}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodSubstrCP() { + assertThat(transform("substrCP(item, 0, 5)"), is(Document.parse("{ \"$substrCP\" : [ \"$item\" , 0 , 5]}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceReverseArray() { + assertThat(transform("reverseArray(array)"), is(Document.parse("{ \"$reverseArray\" : \"$array\"}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + @Ignore("Document API cannot render String[]") + public void shouldRenderMethodReferenceReduce() { + assertThat(transform("reduce(field, '', {'$concat':new String[]{'$$value','$$this'}})"), is(Document.parse( + "{ \"$reduce\" : { \"input\" : \"$field\" , \"initialValue\" : \"\" , \"in\" : { \"$concat\" : [ \"$$value\" , \"$$this\"]}}}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceZip() { + assertThat(transform("zip(new String[]{'$array1', '$array2'})"), + is(Document.parse("{ \"$zip\" : { \"inputs\" : [ \"$array1\" , \"$array2\"]}}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceZipWithOptionalArgs() { + assertThat(transform("zip(new String[]{'$array1', '$array2'}, true, new int[]{1,2})"), is(Document.parse( + "{ \"$zip\" : { \"inputs\" : [ \"$array1\" , \"$array2\"] , \"useLongestLength\" : true , \"defaults\" : [ 1 , 2]}}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodIn() { + assertThat(transform("in('item', array)"), is(Document.parse("{ \"$in\" : [ \"item\" , \"$array\"]}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodRefereneIsoDayOfWeek() { + assertThat(transform("isoDayOfWeek(date)"), is(Document.parse("{ \"$isoDayOfWeek\" : \"$date\"}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodRefereneIsoWeek() { + assertThat(transform("isoWeek(date)"), is(Document.parse("{ \"$isoWeek\" : \"$date\"}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodRefereneIsoWeekYear() { + assertThat(transform("isoWeekYear(date)"), is(Document.parse("{ \"$isoWeekYear\" : \"$date\"}"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodRefereneType() { + assertThat(transform("type(a)"), is(Document.parse("{ \"$type\" : \"$a\"}"))); + } + 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); diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 1bcc9af94..19ee24886 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1688,26 +1688,28 @@ At the time of this writing we provide support for the following Aggregation Ope | abs, add (*via plus), ceil, divide, exp, floor, ln, log, log10, mod, multiply, pow, sqrt, subtract (*via minus), trunc | String Aggregation Operators -| concat, substr, toLower, toUpper, stcasecmp +| concat, substr, toLower, toUpper, stcasecmp, indexOfBytes, indexOfCP, split, strLenBytes, strLenCP, substrCP, | Comparison Aggregation Operators | eq (*via: is), gt, gte, lt, lte, ne | Array Aggregation Operators -| arrayElementAt, concatArrays, filter, isArray, size, slice +| arrayElementAt, concatArrays, filter, in, indexOfArray, isArray, range, reverseArray, reduce, size, slice, zip | Literal Operators | literal | Date Aggregation Operators -| dayOfYear, dayOfMonth, dayOfWeek, year, month, week, hour, minute, second, millisecond, dateToString +| dayOfYear, dayOfMonth, dayOfWeek, year, month, week, hour, minute, second, millisecond, dateToString, isoDayOfWeek, isoWeek, isoWeekYear | Variable Operators | map - | Conditional Aggregation Operators -| cond, ifNull +| cond, ifNull, switch + +| Type Aggregation Operators +| type |===