diff --git a/pom.xml b/pom.xml index ea80a3cb74..19b54876d4 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1548-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index ae0a5d6c8f..08d1e76ce9 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1548-SNAPSHOT ../pom.xml @@ -48,7 +48,7 @@ org.springframework.data spring-data-mongodb - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1548-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 2d02722262..5ab3647b24 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1548-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-log4j/pom.xml b/spring-data-mongodb-log4j/pom.xml index ee5e3336db..0142c1b99a 100644 --- a/spring-data-mongodb-log4j/pom.xml +++ b/spring-data-mongodb-log4j/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1548-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 8072d3f665..8bdc1b020d 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1548-SNAPSHOT ../pom.xml 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 2b5e873747..0c86caf1e7 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 @@ -23,10 +23,14 @@ import java.util.List; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Range; +import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ArithmeticOperators.ArithmeticOperatorFactory; 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.query.CriteriaDefinition; import org.springframework.util.Assert; @@ -262,6 +266,30 @@ public static IfNull.ThenBuilder ifNull(AggregationExpression expression) { return IfNull.ifNull(expression); } + /** + * Creates new {@link AggregationExpression} that evaluates a series of {@link CaseOperator} expressions. When it + * finds an expression which evaluates to {@literal 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 {@literal 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; @@ -1564,6 +1592,184 @@ public StrCaseCmp strCaseCmpValueOf(AggregationExpression expression) { 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 occurrence of a given {@literal substring} and returns the UTF-8 byte index (zero-based) of the + * first occurrence. + * + * @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 occurrence of a substring contained in the given {@literal field reference} and returns the UTF-8 + * byte index (zero-based) of the first occurrence. + * + * @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 occurrence of a substring resulting from the given {@link AggregationExpression} and returns the + * UTF-8 byte index (zero-based) of the first occurrence. + * + * @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 occurrence of a given {@literal substring} and returns the UTF-8 code point index (zero-based) of + * the first occurrence. + * + * @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 occurrence of a substring contained in the given {@literal field reference} and returns the UTF-8 + * code point index (zero-based) of the first occurrence. + * + * @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 occurrence of a substring resulting from the given {@link AggregationExpression} and returns the + * UTF-8 code point index (zero-based) of the first occurrence. + * + * @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); + } } } @@ -1731,6 +1937,101 @@ public Slice slice() { return usesFieldRef() ? Slice.sliceArrayOf(fieldReference) : Slice.sliceArrayOf(expression); } + /** + * Creates new {@link AggregationExpressions} that searches the associated array for an occurrence of a specified + * value and returns the array index (zero-based) of the first occurrence. + * + * @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 associated array. + * + * @param value must not be {@literal null}. + * @return + */ + public In containsValue(Object value) { + return (usesFieldRef() ? In.arrayOf(fieldReference) : In.arrayOf(expression)).containsValue(value); + } + + /** + * @author Christoph Strobl + */ + public interface ReduceInitialValueBuilder { + + /** + * Define the initial cumulative value set before in is applied to the first element of the input array. + * + * @param initialValue must not be {@literal null}. + * @return + */ + Reduce startingWith(Object initialValue); + } + private boolean usesFieldRef() { return fieldReference != null; } @@ -1952,6 +2253,35 @@ public DateToString toString(String format) { .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; } @@ -2022,6 +2352,9 @@ protected AbstractAggregationExpression(Object value) { this.value = value; } + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override public DBObject toDbObject(AggregationOperationContext context) { return toDbObject(this.value, context); @@ -2072,6 +2405,17 @@ private Object unpack(Object value, AggregationOperationContext context) { 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; } @@ -2094,17 +2438,29 @@ protected List append(Object value) { 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(); } @@ -3442,6 +3798,10 @@ public static Trunc truncValueOf(Number value) { } } + // ######################################### + // STRING OPERATORS + // ######################################### + /** * {@link AggregationExpression} for {@code $concat}. * @@ -3736,503 +4096,1633 @@ public StrCaseCmp strcasecmpValueOf(AggregationExpression expression) { } /** - * {@link AggregationExpression} for {@code $arrayElementAt}. + * {@link AggregationExpression} for {@code $indexOfBytes}. * * @author Christoph Strobl */ - class ArrayElemAt extends AbstractAggregationExpression { + class IndexOfBytes extends AbstractAggregationExpression { - private ArrayElemAt(List value) { + private IndexOfBytes(List value) { super(value); } @Override protected String getMongoMethod() { - return "$arrayElemAt"; + return "$indexOfBytes"; } /** - * Creates new {@link ArrayElemAt}. + * Start creating a new {@link IndexOfBytes}. * * @param fieldReference must not be {@literal null}. * @return */ - public static ArrayElemAt arrayOf(String fieldReference) { + public static SubstringBuilder valueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new ArrayElemAt(asFields(fieldReference)); + return new SubstringBuilder(Fields.field(fieldReference)); } /** - * Creates new {@link ArrayElemAt}. + * Start creating a new {@link IndexOfBytes}. * * @param expression must not be {@literal null}. * @return */ - public static ArrayElemAt arrayOf(AggregationExpression expression) { + public static SubstringBuilder valueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); - return new ArrayElemAt(Collections.singletonList(expression)); + return new SubstringBuilder(expression); } - public ArrayElemAt elementAt(int index) { - return new ArrayElemAt(append(index)); - } + /** + * Optionally define the substring search start and end position. + * + * @param range must not be {@literal null}. + * @return + */ + public IndexOfBytes within(Range range) { - public ArrayElemAt elementAt(AggregationExpression expression) { + Assert.notNull(range, "Range must not be null!"); - Assert.notNull(expression, "Expression must not be null!"); - return new ArrayElemAt(append(expression)); + List rangeValues = new ArrayList(2); + rangeValues.add(range.getLowerBound()); + if (range.getUpperBound() != null) { + rangeValues.add(range.getUpperBound()); + } + + return new IndexOfBytes(append(rangeValues)); } - public ArrayElemAt elementAt(String arrayFieldReference) { + public static class SubstringBuilder { - Assert.notNull(arrayFieldReference, "ArrayReference must not be null!"); - return new ArrayElemAt(append(Fields.field(arrayFieldReference))); + private final Object stringExpression; + + private SubstringBuilder(Object stringExpression) { + this.stringExpression = stringExpression; + } + + /** + * Creates a new {@link IndexOfBytes} given {@literal substring}. + * + * @param substring must not be {@literal null}. + * @return + */ + public IndexOfBytes indexOf(String substring) { + return new IndexOfBytes(Arrays.asList(stringExpression, substring)); + } + + /** + * Creates a new {@link IndexOfBytes} given {@link AggregationExpression} that resolves to the substring. + * + * @param expression must not be {@literal null}. + * @return + */ + public IndexOfBytes indexOf(AggregationExpression expression) { + return new IndexOfBytes(Arrays.asList(stringExpression, expression)); + } + + /** + * Creates a new {@link IndexOfBytes} given {@link Field} that resolves to the substring. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public IndexOfBytes indexOf(Field fieldReference) { + return new IndexOfBytes(Arrays.asList(stringExpression, fieldReference)); + } } } /** - * {@link AggregationExpression} for {@code $concatArrays}. + * {@link AggregationExpression} for {@code $indexOfCP}. * * @author Christoph Strobl */ - class ConcatArrays extends AbstractAggregationExpression { + class IndexOfCP extends AbstractAggregationExpression { - private ConcatArrays(List value) { + private IndexOfCP(List value) { super(value); } @Override protected String getMongoMethod() { - return "$concatArrays"; + return "$indexOfCP"; } /** - * Creates new {@link ConcatArrays}. + * Start creating a new {@link IndexOfCP}. * * @param fieldReference must not be {@literal null}. * @return */ - public static ConcatArrays arrayOf(String fieldReference) { + public static SubstringBuilder valueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new ConcatArrays(asFields(fieldReference)); + return new SubstringBuilder(Fields.field(fieldReference)); } /** - * Creates new {@link ConcatArrays}. + * Start creating a new {@link IndexOfCP}. * * @param expression must not be {@literal null}. * @return */ - public static ConcatArrays arrayOf(AggregationExpression expression) { + public static SubstringBuilder valueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); - return new ConcatArrays(Collections.singletonList(expression)); + return new SubstringBuilder(expression); } - public ConcatArrays concat(String arrayFieldReference) { + /** + * 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; + } + + /** + * Creates a new {@link IndexOfCP} given {@literal substring}. + * + * @param substring must not be {@literal null}. + * @return + */ + public IndexOfCP indexOf(String substring) { + return new IndexOfCP(Arrays.asList(stringExpression, substring)); + } + + /** + * Creates a new {@link IndexOfCP} given {@link AggregationExpression} that resolves to the substring. + * + * @param expression must not be {@literal null}. + * @return + */ + public IndexOfCP indexOf(AggregationExpression expression) { + return new IndexOfCP(Arrays.asList(stringExpression, expression)); + } + + /** + * Creates a new {@link IndexOfCP} given {@link Field} that resolves to the substring. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public IndexOfCP indexOf(Field fieldReference) { + return new IndexOfCP(Arrays.asList(stringExpression, fieldReference)); + } + } + } + + /** + * {@link AggregationExpression} for {@code $split}. + * + * @author Christoph Strobl + */ + 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 delimiter. + * + * @param delimiter must not be {@literal null}. + * @return + */ + public Split split(String delimiter) { + + Assert.notNull(delimiter, "Delimiter must not be null!"); + return new Split(append(delimiter)); + } + + /** + * Use value of referenced field as delimiter. + * + * @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 delimiter. + * + * @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}. + * + * @author Christoph Strobl + */ + class StrLenBytes extends AbstractAggregationExpression { + + private StrLenBytes(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$strLenBytes"; + } + + /** + * Creates new {@link StrLenBytes}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static StrLenBytes stringLengthOf(String fieldReference) { + return new StrLenBytes(Fields.field(fieldReference)); + } + + /** + * Creates new {@link StrLenBytes}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static StrLenBytes stringLengthOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new StrLenBytes(expression); + } + } + + /** + * {@link AggregationExpression} for {@code $strLenCP}. + * + * @author Christoph Strobl + */ + class StrLenCP extends AbstractAggregationExpression { + + private StrLenCP(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$strLenCP"; + } + + /** + * Creates new {@link StrLenCP}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static StrLenCP stringLengthOfCP(String fieldReference) { + return new StrLenCP(Fields.field(fieldReference)); + } + + /** + * Creates new {@link StrLenCP}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static StrLenCP stringLengthOfCP(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + 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}. + * + * @author Christoph Strobl + */ + class ArrayElemAt extends AbstractAggregationExpression { + + private ArrayElemAt(List value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$arrayElemAt"; + } + + /** + * Creates new {@link ArrayElemAt}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static ArrayElemAt arrayOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new ArrayElemAt(asFields(fieldReference)); + } + + /** + * Creates new {@link ArrayElemAt}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static ArrayElemAt arrayOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new ArrayElemAt(Collections.singletonList(expression)); + } + + public ArrayElemAt elementAt(int index) { + return new ArrayElemAt(append(index)); + } + + public ArrayElemAt elementAt(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new ArrayElemAt(append(expression)); + } + + public ArrayElemAt elementAt(String arrayFieldReference) { + + Assert.notNull(arrayFieldReference, "ArrayReference must not be null!"); + return new ArrayElemAt(append(Fields.field(arrayFieldReference))); + } + } + + /** + * {@link AggregationExpression} for {@code $concatArrays}. + * + * @author Christoph Strobl + */ + class ConcatArrays extends AbstractAggregationExpression { + + private ConcatArrays(List value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$concatArrays"; + } + + /** + * Creates new {@link ConcatArrays}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static ConcatArrays arrayOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new ConcatArrays(asFields(fieldReference)); + } + + /** + * Creates new {@link ConcatArrays}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static ConcatArrays arrayOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new ConcatArrays(Collections.singletonList(expression)); + } + + public ConcatArrays concat(String arrayFieldReference) { + + Assert.notNull(arrayFieldReference, "ArrayFieldReference must not be null!"); + return new ConcatArrays(append(Fields.field(arrayFieldReference))); + } + + public ConcatArrays concat(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new ConcatArrays(append(expression)); + } + } + + /** + * {@code $filter} {@link AggregationExpression} allows to select a subset of the array to return based on the + * specified condition. + * + * @author Christoph Strobl + * @since 1.10 + */ + class Filter implements AggregationExpression { + + private Object input; + private ExposedField as; + private Object condition; + + private Filter() { + // used by builder + } + + /** + * Set the {@literal field} to apply the {@code $filter} to. + * + * @param field must not be {@literal null}. + * @return never {@literal null}. + */ + public static AsBuilder filter(String field) { + + Assert.notNull(field, "Field must not be null!"); + return filter(Fields.field(field)); + } + + /** + * Set the {@literal field} to apply the {@code $filter} to. + * + * @param field must not be {@literal null}. + * @return never {@literal null}. + */ + public static AsBuilder filter(Field field) { + + Assert.notNull(field, "Field must not be null!"); + return new FilterExpressionBuilder().filter(field); + } + + /** + * Set the {@literal values} to apply the {@code $filter} to. + * + * @param values must not be {@literal null}. + * @return + */ + public static AsBuilder filter(List values) { + + Assert.notNull(values, "Values must not be null!"); + return new FilterExpressionBuilder().filter(values); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public DBObject toDbObject(final AggregationOperationContext context) { + return toFilter(ExposedFields.from(as), context); + } + + private DBObject toFilter(ExposedFields exposedFields, AggregationOperationContext context) { + + DBObject filterExpression = new BasicDBObject(); + InheritingExposedFieldsAggregationOperationContext operationContext = new InheritingExposedFieldsAggregationOperationContext( + exposedFields, context); + + filterExpression.putAll(context.getMappedObject(new BasicDBObject("input", getMappedInput(context)))); + filterExpression.put("as", as.getTarget()); + + filterExpression.putAll(context.getMappedObject(new BasicDBObject("cond", getMappedCondition(operationContext)))); + + return new BasicDBObject("$filter", filterExpression); + } + + private Object getMappedInput(AggregationOperationContext context) { + return input instanceof Field ? context.getReference((Field) input).toString() : input; + } + + private Object getMappedCondition(AggregationOperationContext context) { + + if (!(condition instanceof AggregationExpression)) { + return condition; + } + + NestedDelegatingExpressionAggregationOperationContext nea = new NestedDelegatingExpressionAggregationOperationContext( + context); + return ((AggregationExpression) condition).toDbObject(nea); + } + + /** + * @author Christoph Strobl + */ + public interface InputBuilder { + + /** + * Set the {@literal values} to apply the {@code $filter} to. + * + * @param array must not be {@literal null}. + * @return + */ + AsBuilder filter(List array); + + /** + * Set the {@literal field} holding an array to apply the {@code $filter} to. + * + * @param field must not be {@literal null}. + * @return + */ + AsBuilder filter(Field field); + } + + /** + * @author Christoph Strobl + */ + public interface AsBuilder { + + /** + * Set the {@literal variableName} for the elements in the input array. + * + * @param variableName must not be {@literal null}. + * @return + */ + ConditionBuilder as(String variableName); + } + + /** + * @author Christoph Strobl + */ + public interface ConditionBuilder { + + /** + * Set the {@link AggregationExpression} that determines whether to include the element in the resulting array. + * + * @param expression must not be {@literal null}. + * @return + */ + Filter by(AggregationExpression expression); + + /** + * Set the {@literal expression} that determines whether to include the element in the resulting array. + * + * @param expression must not be {@literal null}. + * @return + */ + Filter by(String expression); + + /** + * Set the {@literal expression} that determines whether to include the element in the resulting array. + * + * @param expression must not be {@literal null}. + * @return + */ + Filter by(DBObject expression); + } + + /** + * @author Christoph Strobl + */ + static final class FilterExpressionBuilder implements InputBuilder, AsBuilder, ConditionBuilder { + + private final Filter filter; + + FilterExpressionBuilder() { + this.filter = new Filter(); + } + + public static InputBuilder newBuilder() { + return new FilterExpressionBuilder(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.InputBuilder#filter(java.util.List) + */ + @Override + public AsBuilder filter(List array) { + + Assert.notNull(array, "Array must not be null!"); + filter.input = new ArrayList(array); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.InputBuilder#filter(org.springframework.data.mongodb.core.aggregation.Field) + */ + @Override + public AsBuilder filter(Field field) { + + Assert.notNull(field, "Field must not be null!"); + filter.input = field; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.AsBuilder#as(java.lang.String) + */ + @Override + public ConditionBuilder as(String variableName) { + + Assert.notNull(variableName, "Variable name must not be null!"); + filter.as = new ExposedField(variableName, true); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.ConditionBuilder#by(org.springframework.data.mongodb.core.aggregation.AggregationExpression) + */ + @Override + public Filter by(AggregationExpression condition) { + + Assert.notNull(condition, "Condition must not be null!"); + filter.condition = condition; + return filter; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.ConditionBuilder#by(java.lang.String) + */ + @Override + public Filter by(String expression) { + + Assert.notNull(expression, "Expression must not be null!"); + filter.condition = expression; + return filter; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.ConditionBuilder#by(com.mongodb.DBObject) + */ + @Override + public Filter by(DBObject expression) { + + Assert.notNull(expression, "Expression must not be null!"); + filter.condition = expression; + return filter; + } + } + } + + /** + * {@link AggregationExpression} for {@code $isArray}. + * + * @author Christoph Strobl + */ + class IsArray extends AbstractAggregationExpression { + + private IsArray(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$isArray"; + } + + /** + * Creates new {@link IsArray}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static IsArray isArray(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new IsArray(Fields.field(fieldReference)); + } + + /** + * Creates new {@link IsArray}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static IsArray isArray(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null!"); + return new IsArray(expression); + } + } + + /** + * {@link AggregationExpression} for {@code $size}. + * + * @author Christoph Strobl + */ + class Size extends AbstractAggregationExpression { + + private Size(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$size"; + } + + /** + * Creates new {@link Size}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static Size lengthOfArray(String fieldReference) { - Assert.notNull(arrayFieldReference, "ArrayFieldReference must not be null!"); - return new ConcatArrays(append(Fields.field(arrayFieldReference))); + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new Size(Fields.field(fieldReference)); } - public ConcatArrays concat(AggregationExpression expression) { + /** + * Creates new {@link Size}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static Size lengthOfArray(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); - return new ConcatArrays(append(expression)); + return new Size(expression); } } /** - * {@code $filter} {@link AggregationExpression} allows to select a subset of the array to return based on the - * specified condition. + * {@link AggregationExpression} for {@code $slice}. * * @author Christoph Strobl - * @since 1.10 */ - class Filter implements AggregationExpression { + class Slice extends AbstractAggregationExpression { - private Object input; - private ExposedField as; - private Object condition; + private Slice(List value) { + super(value); + } - private Filter() { - // used by builder + @Override + protected String getMongoMethod() { + return "$slice"; } /** - * Set the {@literal field} to apply the {@code $filter} to. + * Creates new {@link Slice}. * - * @param field must not be {@literal null}. - * @return never {@literal null}. + * @param fieldReference must not be {@literal null}. + * @return */ - public static AsBuilder filter(String field) { + public static Slice sliceArrayOf(String fieldReference) { - Assert.notNull(field, "Field must not be null!"); - return filter(Fields.field(field)); + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new Slice(asFields(fieldReference)); } /** - * Set the {@literal field} to apply the {@code $filter} to. + * Creates new {@link Slice}. * - * @param field must not be {@literal null}. - * @return never {@literal null}. + * @param expression must not be {@literal null}. + * @return */ - public static AsBuilder filter(Field field) { + public static Slice sliceArrayOf(AggregationExpression expression) { - Assert.notNull(field, "Field must not be null!"); - return new FilterExpressionBuilder().filter(field); + Assert.notNull(expression, "Expression must not be null!"); + return new Slice(Collections.singletonList(expression)); + } + + public Slice itemCount(int nrElements) { + return new Slice(append(nrElements)); + } + + public SliceElementsBuilder offset(final int position) { + + return new SliceElementsBuilder() { + + @Override + public Slice itemCount(int nrElements) { + return new Slice(append(position)).itemCount(nrElements); + } + }; + } + + /** + * @author Christoph Strobl + */ + public interface SliceElementsBuilder { + + /** + * Set the number of elements given {@literal nrElements}. + * + * @param nrElements + * @return + */ + Slice itemCount(int nrElements); + } + } + + /** + * {@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)); + } + + /** + * @author Christoph Strobl + */ + public static class IndexOfArrayBuilder { + + private final Object targetArray; + + private IndexOfArrayBuilder(Object targetArray) { + this.targetArray = targetArray; + } + + /** + * Set the {@literal value} to check for its index in the array. + * + * @param value must not be {@literal null}. + * @return + */ + 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"; + } + + /** + * Start creating new {@link RangeOperator}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static RangeOperatorBuilder rangeStartingAt(String fieldReference) { + return new RangeOperatorBuilder(Fields.field(fieldReference)); + } + + /** + * Start creating new {@link RangeOperator}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static RangeOperatorBuilder rangeStartingAt(AggregationExpression expression) { + return new RangeOperatorBuilder(expression); + } + + /** + * Start creating new {@link RangeOperator}. + * + * @param value + * @return + */ + 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; + } + + /** + * Creates new {@link RangeOperator}. + * + * @param index + * @return + */ + public RangeOperator to(long index) { + return new RangeOperator(Arrays.asList(startPoint, index)); + } + + /** + * Creates new {@link RangeOperator}. + * + * @param expression must not be {@literal null}. + * @return + */ + public RangeOperator to(AggregationExpression expression) { + return new RangeOperator(Arrays.asList(startPoint, expression)); + } + + /** + * Creates new {@link RangeOperator}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public RangeOperator to(String fieldReference) { + return new RangeOperator(Arrays.asList(startPoint, Fields.field(fieldReference))); + } + } + } + + /** + * {@link AggregationExpression} for {@code $reverseArray}. + * + * @author Christoph Strobl + */ + class ReverseArray extends AbstractAggregationExpression { + + private ReverseArray(Object value) { + super(value); + } + + @Override + protected String getMongoMethod() { + return "$reverseArray"; + } + + /** + * Creates new {@link ReverseArray} given {@literal fieldReference}. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static ReverseArray reverseArrayOf(String fieldReference) { + return new ReverseArray(Fields.field(fieldReference)); + } + + /** + * Creates new {@link ReverseArray} given {@link AggregationExpression}. + * + * @param expression must not be {@literal null}. + * @return + */ + public static ReverseArray reverseArrayOf(AggregationExpression expression) { + return new ReverseArray(expression); + } + } + + /** + * {@link AggregationExpression} for {@code $reduce}. + * + * @author Christoph Strobl + */ + 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; + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public DBObject toDbObject(AggregationOperationContext context) { + + DBObject dbo = new BasicDBObject(); + + dbo.put("input", getMappedValue(input, context)); + dbo.put("initialValue", getMappedValue(initialValue, context)); + + if (reduceExpressions.iterator().next() instanceof PropertyExpression) { + + DBObject properties = new BasicDBObject(); + for (AggregationExpression e : reduceExpressions) { + properties.putAll(e.toDbObject(context)); + } + dbo.put("in", properties); + } else { + dbo.put("in", (reduceExpressions.iterator().next()).toDbObject(context)); + } + + return new BasicDBObject("$reduce", dbo); + } + + private Object getMappedValue(Object value, AggregationOperationContext context) { + + if (value instanceof DBObject) { + return value; + } + if (value instanceof AggregationExpression) { + return ((AggregationExpression) value).toDbObject(context); + } else if (value instanceof Field) { + return context.getReference(((Field) value)).toString(); + } else { + return context.getMappedObject(new BasicDBObject("###val###", value)).get("###val###"); + } } /** - * Set the {@literal values} to apply the {@code $filter} to. + * Start creating new {@link Reduce}. * - * @param values must not be {@literal null}. + * @param fieldReference must not be {@literal null}. * @return */ - public static AsBuilder filter(List values) { - - Assert.notNull(values, "Values must not be null!"); - return new FilterExpressionBuilder().filter(values); - } + public static InitialValueBuilder arrayOf(final String fieldReference) { - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) - */ - @Override - public DBObject toDbObject(final AggregationOperationContext context) { - return toFilter(ExposedFields.from(as), context); - } + Assert.notNull(fieldReference, "FieldReference must not be null"); - private DBObject toFilter(ExposedFields exposedFields, AggregationOperationContext context) { + return new InitialValueBuilder() { - DBObject filterExpression = new BasicDBObject(); - InheritingExposedFieldsAggregationOperationContext operationContext = new InheritingExposedFieldsAggregationOperationContext( - exposedFields, context); + @Override + public ReduceBuilder withInitialValue(final Object initialValue) { - filterExpression.putAll(context.getMappedObject(new BasicDBObject("input", getMappedInput(context)))); - filterExpression.put("as", as.getTarget()); + Assert.notNull(initialValue, "Initial value must not be null"); - filterExpression.putAll(context.getMappedObject(new BasicDBObject("cond", getMappedCondition(operationContext)))); + return new ReduceBuilder() { - return new BasicDBObject("$filter", filterExpression); - } + @Override + public Reduce reduce(AggregationExpression expression) { - private Object getMappedInput(AggregationOperationContext context) { - return input instanceof Field ? context.getReference((Field) input).toString() : input; - } + Assert.notNull(expression, "AggregationExpression must not be null"); + return new Reduce(Fields.field(fieldReference), initialValue, Collections.singletonList(expression)); + } - private Object getMappedCondition(AggregationOperationContext context) { + @Override + public Reduce reduce(PropertyExpression... expressions) { - if (!(condition instanceof AggregationExpression)) { - return condition; - } + Assert.notNull(expressions, "PropertyExpressions must not be null"); - NestedDelegatingExpressionAggregationOperationContext nea = new NestedDelegatingExpressionAggregationOperationContext( - context); - return ((AggregationExpression) condition).toDbObject(nea); + return new Reduce(Fields.field(fieldReference), initialValue, + Arrays. asList(expressions)); + } + }; + } + }; } /** - * @author Christoph Strobl + * Start creating new {@link Reduce}. + * + * @param expression must not be {@literal null}. + * @return */ - public interface InputBuilder { + public static InitialValueBuilder arrayOf(final AggregationExpression expression) { - /** - * Set the {@literal values} to apply the {@code $filter} to. - * - * @param array must not be {@literal null}. - * @return - */ - AsBuilder filter(List array); + Assert.notNull(expression, "AggregationExpression must not be null"); - /** - * Set the {@literal field} holding an array to apply the {@code $filter} to. - * - * @param field must not be {@literal null}. - * @return - */ - AsBuilder filter(Field field); + return new InitialValueBuilder() { + + @Override + public ReduceBuilder withInitialValue(final Object initialValue) { + + Assert.notNull(initialValue, "Initial value must not be null"); + + return new ReduceBuilder() { + + @Override + public Reduce reduce(AggregationExpression expression) { + + Assert.notNull(expression, "AggregationExpression must not be null"); + return new Reduce(expression, initialValue, Collections.singletonList(expression)); + } + + @Override + public Reduce reduce(PropertyExpression... expressions) { + + Assert.notNull(expressions, "PropertyExpressions must not be null"); + return new Reduce(expression, initialValue, Arrays. asList(expressions)); + } + }; + } + }; } /** * @author Christoph Strobl */ - public interface AsBuilder { + public interface InitialValueBuilder { /** - * Set the {@literal variableName} for the elements in the input array. + * Define the initial cumulative value set before in is applied to the first element of the input array. * - * @param variableName must not be {@literal null}. + * @param initialValue must not be {@literal null}. * @return */ - ConditionBuilder as(String variableName); + ReduceBuilder withInitialValue(Object initialValue); } /** * @author Christoph Strobl */ - public interface ConditionBuilder { - - /** - * Set the {@link AggregationExpression} that determines whether to include the element in the resulting array. - * - * @param expression must not be {@literal null}. - * @return - */ - Filter by(AggregationExpression expression); + public interface ReduceBuilder { /** - * Set the {@literal expression} that determines whether to include the element in the resulting array. + * Define the {@link AggregationExpression} to apply to each element in the input array in left-to-right order. + *
+ * NOTE: During evaluation of the in expression the variable references {@link Variable#THIS} and + * {@link Variable#VALUE} are available. * * @param expression must not be {@literal null}. * @return */ - Filter by(String expression); + Reduce reduce(AggregationExpression expression); /** - * Set the {@literal expression} that determines whether to include the element in the resulting array. + * Define the {@link PropertyExpression}s to apply to each element in the input array in left-to-right order. + *
+ * NOTE: During evaluation of the in expression the variable references {@link Variable#THIS} and + * {@link Variable#VALUE} are available. * * @param expression must not be {@literal null}. * @return */ - Filter by(DBObject expression); + Reduce reduce(PropertyExpression... expressions); } /** * @author Christoph Strobl */ - static final class FilterExpressionBuilder implements InputBuilder, AsBuilder, ConditionBuilder { + public static class PropertyExpression implements AggregationExpression { - private final Filter filter; + private final String propertyName; + private final AggregationExpression aggregationExpression; - FilterExpressionBuilder() { - this.filter = new Filter(); - } + protected PropertyExpression(String propertyName, AggregationExpression aggregationExpression) { - public static InputBuilder newBuilder() { - return new FilterExpressionBuilder(); + Assert.notNull(propertyName, "Property name must not be null!"); + Assert.notNull(aggregationExpression, "AggregationExpression must not be null!"); + + this.propertyName = propertyName; + this.aggregationExpression = aggregationExpression; } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.InputBuilder#filter(java.util.List) + /** + * Define a result property for an {@link AggregationExpression} used in {@link Reduce}. + * + * @param name must not be {@literal null}. + * @return */ - @Override - public AsBuilder filter(List array) { + public static AsBuilder property(final String name) { - Assert.notNull(array, "Array must not be null!"); - filter.input = new ArrayList(array); - return this; + return new AsBuilder() { + + @Override + public PropertyExpression definedAs(AggregationExpression expression) { + return new PropertyExpression(name, expression); + } + }; } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.InputBuilder#filter(org.springframework.data.mongodb.core.aggregation.Field) + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) */ @Override - public AsBuilder filter(Field field) { - - Assert.notNull(field, "Field must not be null!"); - filter.input = field; - return this; + public DBObject toDbObject(AggregationOperationContext context) { + return new BasicDBObject(propertyName, aggregationExpression.toDbObject(context)); } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.AsBuilder#as(java.lang.String) + /** + * @author Christoph Strobl */ - @Override - public ConditionBuilder as(String variableName) { + interface AsBuilder { - Assert.notNull(variableName, "Variable name must not be null!"); - filter.as = new ExposedField(variableName, true); - return this; + /** + * Set the {@link AggregationExpression} resulting in the properties value. + * + * @param expression must not be {@literal null}. + * @return + */ + PropertyExpression definedAs(AggregationExpression expression); } + } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.ConditionBuilder#by(org.springframework.data.mongodb.core.aggregation.AggregationExpression) - */ - @Override - public Filter by(AggregationExpression condition) { + public enum Variable implements Field { - Assert.notNull(condition, "Condition must not be null!"); - filter.condition = condition; - return filter; - } + THIS { + @Override + public String getName() { + return "$$this"; + } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.ConditionBuilder#by(java.lang.String) - */ - @Override - public Filter by(String expression) { + @Override + public String getTarget() { + return "$$this"; + } - Assert.notNull(expression, "Expression must not be null!"); - filter.condition = expression; - return filter; - } + @Override + public boolean isAliased() { + return false; + } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.ConditionBuilder#by(com.mongodb.DBObject) + @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 */ - @Override - public Filter by(DBObject expression) { + public Field referringTo(final String property) { - Assert.notNull(expression, "Expression must not be null!"); - filter.condition = expression; - return filter; + 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 $isArray}. + * {@link AggregationExpression} for {@code $zip}. * * @author Christoph Strobl */ - class IsArray extends AbstractAggregationExpression { + class Zip extends AbstractAggregationExpression { - private IsArray(Object value) { + protected Zip(java.util.Map value) { super(value); } @Override protected String getMongoMethod() { - return "$isArray"; + return "$zip"; } /** - * Creates new {@link IsArray}. + * Start creating new {@link Zip}. * * @param fieldReference must not be {@literal null}. * @return */ - public static IsArray isArray(String fieldReference) { + public static ZipBuilder arrayOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new IsArray(Fields.field(fieldReference)); + return new ZipBuilder(Fields.field(fieldReference)); } /** - * Creates new {@link IsArray}. + * Start creating new {@link Zip}. * * @param expression must not be {@literal null}. * @return */ - public static IsArray isArray(AggregationExpression expression) { + public static ZipBuilder arrayOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); - return new IsArray(expression); + return new ZipBuilder(expression); } - } - /** - * {@link AggregationExpression} for {@code $size}. - * - * @author Christoph Strobl - */ - class Size extends AbstractAggregationExpression { + /** + * Create new {@link Zip} and set the {@code useLongestLength} property to {@literal true}. + * + * @return + */ + public Zip useLongestLength() { + return new Zip(append("useLongestLength", true)); + } - private Size(Object value) { - super(value); + /** + * 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))); } - @Override - protected String getMongoMethod() { - return "$size"; + /** + * 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)); } /** - * Creates new {@link Size}. + * Optionally provide a default value. * - * @param fieldReference must not be {@literal null}. + * @param array must not be {@literal null}. * @return */ - public static Size lengthOfArray(String fieldReference) { + public Zip defaultTo(Object[] array) { + + Assert.notNull(array, "Array must not be null!"); + return new Zip(append("defaults", array)); + } + + public static class ZipBuilder { + + private final List sourceArrays; + + private 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(fieldReference, "FieldReference must not be null!"); - return new Size(Fields.field(fieldReference)); - } + Assert.notNull(arrays, "Arrays must not be null!"); + for (Object value : arrays) { - /** - * Creates new {@link Size}. - * - * @param expression must not be {@literal null}. - * @return - */ - public static Size lengthOfArray(AggregationExpression expression) { + if (value instanceof String) { + sourceArrays.add(Fields.field((String) value)); + } else { + sourceArrays.add(value); + } + } - Assert.notNull(expression, "Expression must not be null!"); - return new Size(expression); + return new Zip(Collections. singletonMap("inputs", sourceArrays)); + } } } /** - * {@link AggregationExpression} for {@code $slice}. + * {@link AggregationExpression} for {@code $in}. * * @author Christoph Strobl */ - class Slice extends AbstractAggregationExpression { + class In extends AbstractAggregationExpression { - private Slice(List value) { - super(value); + private In(List values) { + super(values); } @Override protected String getMongoMethod() { - return "$slice"; + return "$in"; } /** - * Creates new {@link Slice}. + * Start creating {@link In}. * * @param fieldReference must not be {@literal null}. * @return */ - public static Slice sliceArrayOf(String fieldReference) { + public static InBuilder arrayOf(final String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new Slice(asFields(fieldReference)); + + 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))); + } + }; } /** - * Creates new {@link Slice}. + * Start creating {@link In}. * * @param expression must not be {@literal null}. * @return */ - public static Slice sliceArrayOf(AggregationExpression expression) { + public static InBuilder arrayOf(final AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); - return new Slice(Collections.singletonList(expression)); - } - - public Slice itemCount(int nrElements) { - return new Slice(append(nrElements)); - } - public SliceElementsBuilder offset(final int position) { - - return new SliceElementsBuilder() { + return new InBuilder() { @Override - public Slice itemCount(int nrElements) { - return new Slice(append(position)).itemCount(nrElements); + public In containsValue(Object value) { + + Assert.notNull(value, "Value must not be null!"); + return new In(Arrays.asList(value, expression)); } }; } - public interface SliceElementsBuilder { - Slice itemCount(int nrElements); + /** + * @author Christoph Strobl + */ + public interface InBuilder { + + /** + * Set the {@literal value} to check for existence in the array. + * + * @param value must not be {@literal value}. + * @return + */ + In containsValue(Object value); } } + // ############ + // LITERAL OPERATORS + // ############ + /** * {@link AggregationExpression} for {@code $literal}. * @@ -4697,7 +6187,9 @@ protected String getMongoMethod() { public static FormatBuilder dateOf(final String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new FormatBuilder() { + @Override public DateToString toString(String format) { @@ -4716,12 +6208,13 @@ public DateToString toString(String format) { public static FormatBuilder dateOf(final AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); + return new FormatBuilder() { + @Override public DateToString toString(String format) { Assert.notNull(format, "Format must not be null!"); - return new DateToString(argumentMap(expression, format)); } }; @@ -4747,6 +6240,129 @@ public interface FormatBuilder { } } + /** + * {@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}. * @@ -4813,6 +6429,9 @@ public Sum and(AggregationExpression expression) { return new Sum(append(expression)); } + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.AbstractAggregationExpression#toDbObject(java.lang.Object, org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override public DBObject toDbObject(Object value, AggregationOperationContext context) { @@ -4892,6 +6511,9 @@ public Avg and(AggregationExpression expression) { return new Avg(append(expression)); } + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.AbstractAggregationExpression#toDbObject(java.lang.Object, org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override public DBObject toDbObject(Object value, AggregationOperationContext context) { @@ -4971,6 +6593,9 @@ public Max and(AggregationExpression expression) { return new Max(append(expression)); } + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.AbstractAggregationExpression#toDbObject(java.lang.Object, org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override public DBObject toDbObject(Object value, AggregationOperationContext context) { @@ -5050,6 +6675,9 @@ public Min and(AggregationExpression expression) { return new Min(append(expression)); } + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.AbstractAggregationExpression#toDbObject(java.lang.Object, org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override public DBObject toDbObject(Object value, AggregationOperationContext context) { @@ -5129,6 +6757,9 @@ public StdDevPop and(AggregationExpression expression) { return new StdDevPop(append(expression)); } + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.AbstractAggregationExpression#toDbObject(java.lang.Object, org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override public DBObject toDbObject(Object value, AggregationOperationContext context) { @@ -5208,6 +6839,9 @@ public StdDevSamp and(AggregationExpression expression) { return new StdDevSamp(append(expression)); } + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.AbstractAggregationExpression#toDbObject(java.lang.Object, org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override public DBObject toDbObject(Object value, AggregationOperationContext context) { @@ -5962,15 +7596,21 @@ private Map(Object sourceArray, String itemVariableName, AggregationExpression f */ static AsBuilder itemsOf(final String fieldReference) { + Assert.notNull(fieldReference, "FieldReference must not be null!"); + return new AsBuilder() { @Override public FunctionBuilder as(final String variableName) { + Assert.notNull(variableName, "VariableName must not be null!"); + return new FunctionBuilder() { @Override public Map andApply(final AggregationExpression expression) { + + Assert.notNull(expression, "AggregationExpression must not be null!"); return new Map(Fields.field(fieldReference), variableName, expression); } }; @@ -5988,15 +7628,21 @@ public Map andApply(final AggregationExpression expression) { */ public static AsBuilder itemsOf(final AggregationExpression source) { + Assert.notNull(source, "AggregationExpression must not be null!"); + return new AsBuilder() { @Override public FunctionBuilder as(final String variableName) { + Assert.notNull(variableName, "VariableName must not be null!"); + return new FunctionBuilder() { @Override public Map andApply(final AggregationExpression expression) { + + Assert.notNull(expression, "AggregationExpression must not be null!"); return new Map(source, variableName, expression); } }; @@ -6004,6 +7650,9 @@ public Map andApply(final AggregationExpression expression) { }; } + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override public DBObject toDbObject(final AggregationOperationContext context) { return toMap(ExposedFields.synthetic(Fields.fields(itemVariableName)), context); @@ -6076,8 +7725,8 @@ private IfNull(Object condition, Object value) { /** * Creates new {@link IfNull}. * - * @param fieldReference the field to check for a {@literal null} value, field reference must not be - * {@literal null}. + * @param fieldReference the field to check for a {@literal null} value, field reference must not be {@literal null} + * . * @return */ public static ThenBuilder ifNull(String fieldReference) { @@ -6725,6 +8374,7 @@ public static LetBuilder define(final Collection variables) Assert.notNull(variables, "Variables must not be null!"); return new LetBuilder() { + @Override public Let andApply(final AggregationExpression expression) { @@ -6745,6 +8395,7 @@ public static LetBuilder define(final ExpressionVariable... variables) { Assert.notNull(variables, "Variables must not be null!"); return new LetBuilder() { + @Override public Let andApply(final AggregationExpression expression) { @@ -6765,6 +8416,9 @@ public interface LetBuilder { Let andApply(AggregationExpression expression); } + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ @Override public DBObject toDbObject(final AggregationOperationContext context) { return toLet(ExposedFields.synthetic(Fields.fields(getVariableNames())), context); @@ -6864,4 +8518,139 @@ public ExpressionVariable forExpression(DBObject expressionObject) { } } } + + /** + * {@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"; + } + + /** + * Creates new {@link Switch}. + * + * @param conditions must not be {@literal null}. + */ + public static Switch switchCases(CaseOperator... conditions) { + + Assert.notNull(conditions, "Conditions must not be null!"); + return switchCases(Arrays.asList(conditions)); + } + + /** + * Creates new {@link Switch}. + * + * @param conditions must not be {@literal null}. + */ + 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)); + } + + /** + * Encapsulates the aggregation framework case document inside a {@code $switch}-operation. + */ + 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); + } + }; + } + + /* (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public DBObject toDbObject(AggregationOperationContext context) { + + DBObject dbo = new BasicDBObject("case", when.toDbObject(context)); + + if (then instanceof AggregationExpression) { + dbo.put("then", ((AggregationExpression) then).toDbObject(context)); + } else if (then instanceof Field) { + dbo.put("then", context.getReference((Field) then).toString()); + } else { + dbo.put("then", then); + } + + return dbo; + } + + /** + * @author Christoph Strobl + */ + public interface ThenBuilder { + + /** + * Set the then {@literal value}. + * + * @param value must not be {@literal null}. + * @return + */ + 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 f9033743db..ce51f40624 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.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 Object getReferenceValue() { */ @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 183d526520..2ba33412a4 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 Iterator iterator() { } /** - * * @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 List asList() { */ static class AggregationField implements Field { + private final String raw; private final String name; private final String target; @@ -225,6 +226,7 @@ public AggregationField(String name) { */ public AggregationField(String name, String target) { + raw = name; String nameToSet = cleanUp(name); String targetToSet = cleanUp(target); @@ -266,6 +268,11 @@ public String getName() { * @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 boolean isAliased() { 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 d381020b5c..587f0b327e 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 @@ -491,8 +491,10 @@ protected Object convert(AggregationExpressionTransformationContext(5L, 9L))) + .as("byteLocation").toDBObject(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() { + + DBObject agg = project().and(StringOperators.valueOf("item").indexOfCP("foo")).as("cpLocation") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project: { cpLocation: { $indexOfCP: [ \"$item\", \"foo\" ] } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIndexOfCPWithRangeCorrectly() { + + DBObject agg = project().and(StringOperators.valueOf("item").indexOfCP("foo").within(new Range(5L, 9L))) + .as("cpLocation").toDBObject(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() { + + DBObject agg = project().and(StringOperators.valueOf("city").split(", ")).as("city_state") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project : { city_state : { $split: [\"$city\", \", \"] }} }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderStrLenBytesCorrectly() { + + DBObject agg = project().and(StringOperators.valueOf("name").length()).as("length") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project : { \"length\": { $strLenBytes: \"$name\" } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderStrLenCPCorrectly() { + + DBObject agg = project().and(StringOperators.valueOf("name").lengthCP()).as("length") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project : { \"length\": { $strLenCP: \"$name\" } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderSubstrCPCorrectly() { + + DBObject agg = project().and(StringOperators.valueOf("quarter").substringCP(0, 2)).as("yearSubstring") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project : { yearSubstring: { $substrCP: [ \"$quarter\", 0, 2 ] } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIndexOfArrayCorrectly() { + + DBObject agg = project().and(ArrayOperators.arrayOf("items").indexOf(2)).as("index") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project : { index: { $indexOfArray: [ \"$items\", 2 ] } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderRangeCorrectly() { + + DBObject agg = project().and(RangeOperator.rangeStartingAt(0L).to("distance").withStepSize(25L)).as("rest_stops") + .toDBObject(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() { + + DBObject agg = project().and(ArrayOperators.arrayOf("favorites").reverse()).as("reverseFavorites") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project : { reverseFavorites: { $reverseArray: \"$favorites\" } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderReduceWithSimpleObjectCorrectly() { + + DBObject agg = project() + .and(ArrayOperators.arrayOf("probabilityArr") + .reduce(ArithmeticOperators.valueOf("$$value").multiplyBy("$$this")).startingWith(1)) + .as("results").toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.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.referringTo("sum").getName()).add(Variable.THIS.getName())); + PropertyExpression product = PropertyExpression.property("product").definedAs(ArithmeticOperators + .valueOf(Variable.VALUE.referringTo("product").getName()).multiplyBy(Variable.THIS.getName())); + + DBObject agg = project() + .and(ArrayOperators.arrayOf("probabilityArr").reduce(sum, product) + .startingWith(new BasicDBObjectBuilder().add("sum", 5).add("product", 2).get())) + .as("results").toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.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); + + DBObject agg = project().and( + ArrayOperators.arrayOf(elemAt0).zipWith(elemAt1, elemAt2).useLongestLength().defaultTo(new Object[] { 1, 2 })) + .as("transposed").toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.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() { + + DBObject agg = project().and(ArrayOperators.arrayOf("in_stock").containsValue("bananas")).as("has_bananas") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project : { has_bananas : { $in : [\"bananas\", \"$in_stock\" ] } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIsoDayOfWeekCorrectly() { + + DBObject agg = project().and(DateOperators.dateOf("birthday").isoDayOfWeek()).as("dayOfWeek") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project : { dayOfWeek: { $isoDayOfWeek: \"$birthday\" } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIsoWeekCorrectly() { + + DBObject agg = project().and(DateOperators.dateOf("date").isoWeek()).as("weekNumber") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project : { weekNumber: { $isoWeek: \"$date\" } } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderIsoWeekYearCorrectly() { + + DBObject agg = project().and(DateOperators.dateOf("date").isoWeekYear()).as("yearNumber") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.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."); + + DBObject agg = project().and(ConditionalOperators.switchCases(cond1, cond2, cond3).defaultTo("No scores found.")) + .as("summary").toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project : { summary: {" + expected + "} } }"))); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldTypeCorrectly() { + + DBObject agg = project().and(Type.typeOf("a")).as("a") + .toDBObject(Aggregation.DEFAULT_CONTEXT); + + assertThat(agg, Matchers.is(JSON.parse("{ $project : { a: { $type: \"$a\" } } }"))); + } + private static DBObject exctractOperation(String field, DBObject fromProjectClause) { return (DBObject) 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 770145a80a..013d6189e7 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 @@ -745,13 +745,13 @@ public void shouldRenderMethodReferenceNodeMin() { assertThat(transform("min(a, b)"), is("{ \"$min\" : [ \"$a\" , \"$b\"]}")); } - /** * @see DATAMONGO-1530 */ @Test public void shouldRenderMethodReferenceNodePush() { - assertThat(transform("push({'item':'$item', 'quantity':'$qty'})"), is("{ \"$push\" : { \"item\" : \"$item\" , \"quantity\" : \"$qty\"}}")); + assertThat(transform("push({'item':'$item', 'quantity':'$qty'})"), + is("{ \"$push\" : { \"item\" : \"$item\" , \"quantity\" : \"$qty\"}}")); } /** @@ -884,6 +884,129 @@ public void shouldRenderComplexNotCorrectly() { assertThat(transform("!(foo > 10)"), is("{ \"$not\" : [ { \"$gt\" : [ \"$foo\" , 10]}]}")); } + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceIndexOfBytes() { + assertThat(transform("indexOfBytes(item, 'foo')"), is("{ \"$indexOfBytes\" : [ \"$item\" , \"foo\"]}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceIndexOfCP() { + assertThat(transform("indexOfCP(item, 'foo')"), is("{ \"$indexOfCP\" : [ \"$item\" , \"foo\"]}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceSplit() { + assertThat(transform("split(item, ',')"), is("{ \"$split\" : [ \"$item\" , \",\"]}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceStrLenBytes() { + assertThat(transform("strLenBytes(item)"), is("{ \"$strLenBytes\" : \"$item\"}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceStrLenCP() { + assertThat(transform("strLenCP(item)"), is("{ \"$strLenCP\" : \"$item\"}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodSubstrCP() { + assertThat(transform("substrCP(item, 0, 5)"), is("{ \"$substrCP\" : [ \"$item\" , 0 , 5]}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceReverseArray() { + assertThat(transform("reverseArray(array)"), is("{ \"$reverseArray\" : \"$array\"}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceReduce() { + assertThat(transform("reduce(field, '', {'$concat':new String[]{'$$value','$$this'}})"), is( + "{ \"$reduce\" : { \"input\" : \"$field\" , \"initialValue\" : \"\" , \"in\" : { \"$concat\" : [ \"$$value\" , \"$$this\"]}}}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceZip() { + assertThat(transform("zip(new String[]{'$array1', '$array2'})"), + is("{ \"$zip\" : { \"inputs\" : [ \"$array1\" , \"$array2\"]}}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodReferenceZipWithOptionalArgs() { + assertThat(transform("zip(new String[]{'$array1', '$array2'}, true, new int[]{1,2})"), is( + "{ \"$zip\" : { \"inputs\" : [ \"$array1\" , \"$array2\"] , \"useLongestLength\" : true , \"defaults\" : [ 1 , 2]}}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodIn() { + assertThat(transform("in('item', array)"), is("{ \"$in\" : [ \"item\" , \"$array\"]}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodRefereneIsoDayOfWeek() { + assertThat(transform("isoDayOfWeek(date)"), is("{ \"$isoDayOfWeek\" : \"$date\"}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodRefereneIsoWeek() { + assertThat(transform("isoWeek(date)"), is("{ \"$isoWeek\" : \"$date\"}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodRefereneIsoWeekYear() { + assertThat(transform("isoWeekYear(date)"), is("{ \"$isoWeekYear\" : \"$date\"}")); + } + + /** + * @see DATAMONGO-1548 + */ + @Test + public void shouldRenderMethodRefereneType() { + assertThat(transform("type(a)"), is("{ \"$type\" : \"$a\"}")); + } + private String transform(String expression, Object... params) { Object result = transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); return result == null ? null : result.toString(); diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 0b9b03fc0c..d6ff11f630 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1686,26 +1686,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 |===