/* * Copyright 2016. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.mongodb.core.aggregation; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.bson.Document; import org.springframework.data.domain.Range; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Filter.AsBuilder; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Reduce.PropertyExpression; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.util.Assert; /** * Gateway to {@literal array} aggregation operations. * * @author Christoph Strobl * @since 1.0 */ public class ArrayOperators { /** * Take the array referenced by given {@literal fieldReference}. * * @param fieldReference must not be {@literal null}. * @return */ public static ArrayOperatorFactory arrayOf(String fieldReference) { return new ArrayOperatorFactory(fieldReference); } /** * Take the array referenced resulting from the given {@link AggregationExpression}. * * @param expression must not be {@literal null}. * @return */ public static ArrayOperatorFactory arrayOf(AggregationExpression expression) { return new ArrayOperatorFactory(expression); } /** * @author Christoph Strobl */ public static class ArrayOperatorFactory { private final String fieldReference; private final AggregationExpression expression; /** * Creates new {@link ArrayOperatorFactory} for given {@literal fieldReference}. * * @param fieldReference must not be {@literal null}. */ public ArrayOperatorFactory(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null!"); this.fieldReference = fieldReference; this.expression = null; } /** * Creates new {@link ArrayOperatorFactory} for given {@link AggregationExpression}. * * @param expression must not be {@literal null}. */ public ArrayOperatorFactory(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); this.fieldReference = null; this.expression = expression; } /** * Creates new {@link AggregationExpression} that takes the associated array and returns the element at the * specified array {@literal position}. * * @param position * @return */ public ArrayElemAt elementAt(int position) { return createArrayElemAt().elementAt(position); } /** * Creates new {@link AggregationExpression} that takes the associated array and returns the element at the position * resulting form the given {@literal expression}. * * @param expression must not be {@literal null}. * @return */ public ArrayElemAt elementAt(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); return createArrayElemAt().elementAt(expression); } /** * Creates new {@link AggregationExpression} that takes the associated array and returns the element at the position * defined by the referenced {@literal field}. * * @param fieldReference must not be {@literal null}. * @return */ public ArrayElemAt elementAt(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null!"); return createArrayElemAt().elementAt(fieldReference); } private ArrayElemAt createArrayElemAt() { return usesFieldRef() ? ArrayElemAt.arrayOf(fieldReference) : ArrayElemAt.arrayOf(expression); } /** * Creates new {@link AggregationExpression} that takes the associated array and concats the given * {@literal arrayFieldReference} to it. * * @param arrayFieldReference must not be {@literal null}. * @return */ public ConcatArrays concat(String arrayFieldReference) { Assert.notNull(arrayFieldReference, "ArrayFieldReference must not be null!"); return createConcatArrays().concat(arrayFieldReference); } /** * Creates new {@link AggregationExpression} that takes the associated array and concats the array resulting form * the given {@literal expression} to it. * * @param expression must not be {@literal null}. * @return */ public ConcatArrays concat(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); return createConcatArrays().concat(expression); } private ConcatArrays createConcatArrays() { return usesFieldRef() ? ConcatArrays.arrayOf(fieldReference) : ConcatArrays.arrayOf(expression); } /** * Creates new {@link AggregationExpression} that takes the associated array and selects a subset of the array to * return based on the specified condition. * * @return */ public AsBuilder filter() { return Filter.filter(fieldReference); } /** * Creates new {@link AggregationExpression} that takes the associated array and an check if its an array. * * @return */ public IsArray isArray() { return usesFieldRef() ? IsArray.isArray(fieldReference) : IsArray.isArray(expression); } /** * Creates new {@link AggregationExpression} that takes the associated array and retrieves its length. * * @return */ public Size length() { return usesFieldRef() ? Size.lengthOfArray(fieldReference) : Size.lengthOfArray(expression); } /** * Creates new {@link AggregationExpression} that takes the associated array and selects a subset from it. * * @return */ public Slice slice() { return usesFieldRef() ? Slice.sliceArrayOf(fieldReference) : Slice.sliceArrayOf(expression); } /** * Creates new {@link AggregationExpression} 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 AggregationExpression} 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 AggregationExpression} 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 ArrayOperatorFactory.ReduceInitialValueBuilder reduce(final AggregationExpression expression) { return new ArrayOperatorFactory.ReduceInitialValueBuilder() { @Override public Reduce startingWith(Object initialValue) { return (usesFieldRef() ? Reduce.arrayOf(fieldReference) : Reduce.arrayOf(expression)) .withInitialValue(initialValue).reduce(expression); } }; } /** * Start creating new {@link AggregationExpression} that applies an {@link AggregationExpression} to each element in * an array and combines them into a single value. * * @param expressions * @return */ public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(final PropertyExpression... expressions) { return new ArrayOperatorFactory.ReduceInitialValueBuilder() { @Override public Reduce startingWith(Object initialValue) { return (usesFieldRef() ? Reduce.arrayOf(fieldReference) : Reduce.arrayOf(expression)) .withInitialValue(initialValue).reduce(expressions); } }; } /** * Creates new {@link AggregationExpression} 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 AggregationExpression} 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; } } /** * {@link AggregationExpression} for {@code $arrayElementAt}. * * @author Christoph Strobl */ public static 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 */ public static 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 */ public static 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#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) */ @Override public Document toDocument(final AggregationOperationContext context) { return toFilter(ExposedFields.from(as), context); } private Document toFilter(ExposedFields exposedFields, AggregationOperationContext context) { Document filterExpression = new Document(); InheritingExposedFieldsAggregationOperationContext operationContext = new InheritingExposedFieldsAggregationOperationContext( exposedFields, context); filterExpression.putAll(context.getMappedObject(new Document("input", getMappedInput(context)))); filterExpression.put("as", as.getTarget()); filterExpression.putAll(context.getMappedObject(new Document("cond", getMappedCondition(operationContext)))); return new Document("$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).toDocument(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(Document expression); } /** * @author Christoph Strobl */ static final class FilterExpressionBuilder implements InputBuilder, AsBuilder, ConditionBuilder { private final Filter filter; FilterExpressionBuilder() { this.filter = new Filter(); } /** * Creates new {@link InputBuilder}. * * @return */ public static InputBuilder newBuilder() { return new FilterExpressionBuilder(); } /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.ArrayOperators.Filter.InputBuilder#filter(java.util.List) */ @Override public AsBuilder filter(List<?> array) { Assert.notNull(array, "Array must not be null!"); filter.input = new ArrayList<Object>(array); return this; } /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.ArrayOperators.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.ArrayOperators.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.ArrayOperators.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.ArrayOperators.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.ArrayOperators.Filter.ConditionBuilder#by(org.bson.Document) */ @Override public Filter by(Document expression) { Assert.notNull(expression, "Expression must not be null!"); filter.condition = expression; return filter; } } } /** * {@link AggregationExpression} for {@code $isArray}. * * @author Christoph Strobl */ public static 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 */ public static 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(fieldReference, "FieldReference must not be null!"); return new Size(Fields.field(fieldReference)); } /** * 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 Size(expression); } } /** * {@link AggregationExpression} for {@code $slice}. * * @author Christoph Strobl */ public static class Slice extends AbstractAggregationExpression { private Slice(List<?> value) { super(value); } @Override protected String getMongoMethod() { return "$slice"; } /** * Creates new {@link Slice}. * * @param fieldReference must not be {@literal null}. * @return */ public static Slice sliceArrayOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null!"); return new Slice(asFields(fieldReference)); } /** * Creates new {@link Slice}. * * @param expression must not be {@literal null}. * @return */ public static Slice sliceArrayOf(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() { @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 */ public static class IndexOfArray extends AbstractAggregationExpression { private IndexOfArray(List<Object> 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<Long> range) { return new IndexOfArray(append(AggregationUtils.toRangeValues(range))); } /** * @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 */ public static class RangeOperator extends AbstractAggregationExpression { private RangeOperator(List<Object> 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 */ public static 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 */ public static class Reduce implements AggregationExpression { private final Object input; private final Object initialValue; private final List<AggregationExpression> reduceExpressions; private Reduce(Object input, Object initialValue, List<AggregationExpression> reduceExpressions) { this.input = input; this.initialValue = initialValue; this.reduceExpressions = reduceExpressions; } /* (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) */ @Override public Document toDocument(AggregationOperationContext context) { Document document = new Document(); document.put("input", getMappedValue(input, context)); document.put("initialValue", getMappedValue(initialValue, context)); if (reduceExpressions.iterator().next() instanceof PropertyExpression) { Document properties = new Document(); for (AggregationExpression e : reduceExpressions) { properties.putAll(e.toDocument(context)); } document.put("in", properties); } else { document.put("in", (reduceExpressions.iterator().next()).toDocument(context)); } return new Document("$reduce", document); } private Object getMappedValue(Object value, AggregationOperationContext context) { if (value instanceof Document) { return value; } if (value instanceof AggregationExpression) { return ((AggregationExpression) value).toDocument(context); } else if (value instanceof Field) { return context.getReference(((Field) value)).toString(); } else { return context.getMappedObject(new Document("###val###", value)).get("###val###"); } } /** * Start creating new {@link Reduce}. * * @param fieldReference must not be {@literal null}. * @return */ public static InitialValueBuilder arrayOf(final String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); 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(Fields.field(fieldReference), initialValue, Collections.singletonList(expression)); } @Override public Reduce reduce(PropertyExpression... expressions) { Assert.notNull(expressions, "PropertyExpressions must not be null"); return new Reduce(Fields.field(fieldReference), initialValue, Arrays.<AggregationExpression> asList(expressions)); } }; } }; } /** * Start creating new {@link Reduce}. * * @param expression must not be {@literal null}. * @return */ public static InitialValueBuilder arrayOf(final AggregationExpression expression) { Assert.notNull(expression, "AggregationExpression must not be null"); 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.<AggregationExpression> asList(expressions)); } }; } }; } /** * @author Christoph Strobl */ public interface InitialValueBuilder { /** * 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 */ ReduceBuilder withInitialValue(Object initialValue); } /** * @author Christoph Strobl */ public interface ReduceBuilder { /** * Define the {@link AggregationExpression} to apply to each element in the input array in left-to-right order. * <br /> * <b>NOTE:</b> 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 */ Reduce reduce(AggregationExpression expression); /** * Define the {@link PropertyExpression}s to apply to each element in the input array in left-to-right order. * <br /> * <b>NOTE:</b> During evaluation of the in expression the variable references {@link Variable#THIS} and * {@link Variable#VALUE} are available. * * @param expressions must not be {@literal null}. * @return */ Reduce reduce(PropertyExpression... expressions); } /** * @author Christoph Strobl */ public static class PropertyExpression implements AggregationExpression { private final String propertyName; private final AggregationExpression aggregationExpression; protected PropertyExpression(String propertyName, AggregationExpression aggregationExpression) { Assert.notNull(propertyName, "Property name must not be null!"); Assert.notNull(aggregationExpression, "AggregationExpression must not be null!"); this.propertyName = propertyName; this.aggregationExpression = aggregationExpression; } /** * Define a result property for an {@link AggregationExpression} used in {@link Reduce}. * * @param name must not be {@literal null}. * @return */ public static AsBuilder property(final String name) { return new AsBuilder() { @Override public PropertyExpression definedAs(AggregationExpression expression) { return new PropertyExpression(name, expression); } }; } /* (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) */ @Override public Document toDocument(AggregationOperationContext context) { return new Document(propertyName, aggregationExpression.toDocument(context)); } /** * @author Christoph Strobl */ public interface AsBuilder { /** * Set the {@link AggregationExpression} resulting in the properties value. * * @param expression must not be {@literal null}. * @return */ PropertyExpression definedAs(AggregationExpression expression); } } public enum Variable implements Field { THIS { @Override public String getName() { return "$$this"; } @Override public String getTarget() { return "$$this"; } @Override public boolean isAliased() { return false; } @Override public String toString() { return getName(); } }, VALUE { @Override public String getName() { return "$$value"; } @Override public String getTarget() { return "$$value"; } @Override public boolean isAliased() { return false; } @Override public String toString() { return getName(); } }; /** * Create a {@link Field} reference to a given {@literal property} prefixed with the {@link Variable} identifier. * eg. {@code $$value.product} * * @param property must not be {@literal null}. * @return */ public Field referringTo(final String property) { return new Field() { @Override public String getName() { return Variable.this.getName() + "." + property; } @Override public String getTarget() { return Variable.this.getTarget() + "." + property; } @Override public boolean isAliased() { return false; } @Override public String toString() { return getName(); } }; } } } /** * {@link AggregationExpression} for {@code $zip}. * * @author Christoph Strobl */ public static class Zip extends AbstractAggregationExpression { protected Zip(java.util.Map<String, Object> value) { super(value); } @Override protected String getMongoMethod() { return "$zip"; } /** * Start creating new {@link Zip}. * * @param fieldReference must not be {@literal null}. * @return */ public static ZipBuilder arrayOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null!"); return new ZipBuilder(Fields.field(fieldReference)); } /** * Start creating new {@link Zip}. * * @param expression must not be {@literal null}. * @return */ public static ZipBuilder arrayOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); return new ZipBuilder(expression); } /** * Create new {@link Zip} and set the {@code useLongestLength} property to {@literal true}. * * @return */ public Zip useLongestLength() { return new Zip(append("useLongestLength", true)); } /** * Optionally provide a default value. * * @param fieldReference must not be {@literal null}. * @return */ public Zip defaultTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null!"); return new Zip(append("defaults", Fields.field(fieldReference))); } /** * Optionally provide a default value. * * @param expression must not be {@literal null}. * @return */ public Zip defaultTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); return new Zip(append("defaults", expression)); } /** * Optionally provide a default value. * * @param array must not be {@literal null}. * @return */ public Zip defaultTo(Object[] array) { Assert.notNull(array, "Array must not be null!"); return new Zip(append("defaults", Arrays.asList(array))); } public static class ZipBuilder { private final List<Object> sourceArrays; private ZipBuilder(Object sourceArray) { this.sourceArrays = new ArrayList<Object>(); this.sourceArrays.add(sourceArray); } /** * Creates new {@link Zip} that transposes an array of input arrays so that the first element of the output array * would be an array containing, the first element of the first input array, the first element of the second input * array, etc. * * @param arrays arrays to zip the referenced one with. must not be {@literal null}. * @return */ public Zip zip(Object... arrays) { Assert.notNull(arrays, "Arrays must not be null!"); for (Object value : arrays) { if (value instanceof String) { sourceArrays.add(Fields.field((String) value)); } else { sourceArrays.add(value); } } return new Zip(Collections.<String, Object> singletonMap("inputs", sourceArrays)); } } } /** * {@link AggregationExpression} for {@code $in}. * * @author Christoph Strobl */ public static class In extends AbstractAggregationExpression { private In(List<Object> values) { super(values); } @Override protected String getMongoMethod() { return "$in"; } /** * Start creating {@link In}. * * @param fieldReference must not be {@literal null}. * @return */ public static InBuilder arrayOf(final String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null!"); return new InBuilder() { @Override public In containsValue(Object value) { Assert.notNull(value, "Value must not be null!"); return new In(Arrays.asList(value, Fields.field(fieldReference))); } }; } /** * Start creating {@link In}. * * @param expression must not be {@literal null}. * @return */ public static InBuilder arrayOf(final AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null!"); return new InBuilder() { @Override public In containsValue(Object value) { Assert.notNull(value, "Value must not be null!"); return new In(Arrays.asList(value, expression)); } }; } /** * @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); } } }