ArrayOperators.java

/*
 * Copyright 2016-present the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.mongodb.core.aggregation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.bson.Document;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
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.lang.Contract;
import org.springframework.util.Assert;

/**
 * Gateway to {@literal array} aggregation operations.
 *
 * @author Christoph Strobl
 * @author Mark Paluch
 * @author Shashank Sharma
 * @author Divya Srivastava
 * @since 1.0
 */
public class ArrayOperators {

	/**
	 * Take the array referenced by given {@literal fieldReference}.
	 *
	 * @param fieldReference must not be {@literal null}.
	 * @return new instance of {@link ArrayOperatorFactory}.
	 */
	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 new instance of {@link ArrayOperatorFactory}.
	 */
	public static ArrayOperatorFactory arrayOf(AggregationExpression expression) {
		return new ArrayOperatorFactory(expression);
	}

	/**
	 * Take the given {@link Collection values} {@link AggregationExpression}.
	 *
	 * @param values must not be {@literal null}.
	 * @return new instance of {@link ArrayOperatorFactory}.
	 * @since 2.2
	 */
	public static ArrayOperatorFactory arrayOf(Collection<?> values) {
		return new ArrayOperatorFactory(values);
	}

	/**
	 * @author Christoph Strobl
	 */
	public static class ArrayOperatorFactory {

		private final @Nullable String fieldReference;
		private final @Nullable AggregationExpression expression;
		private final @Nullable Collection<?> values;

		/**
		 * 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;
			this.values = 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;
			this.values = null;
		}

		/**
		 * Creates new {@link ArrayOperatorFactory} for given values.
		 *
		 * @param values must not be {@literal null}.
		 * @since 2.2
		 */
		public ArrayOperatorFactory(Collection<?> values) {

			Assert.notNull(values, "Values must not be null");
			this.fieldReference = null;
			this.expression = null;
			this.values = values;
		}

		/**
		 * Creates new {@link AggregationExpression} that takes the associated array and returns the element at the
		 * specified array {@literal position}.
		 *
		 * @param position the element index.
		 * @return new instance of {@link ArrayElemAt}.
		 */
		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 new instance of {@link ArrayElemAt}.
		 */
		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 new instance of {@link ArrayElemAt}.
		 */
		public ArrayElemAt elementAt(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null");
			return createArrayElemAt().elementAt(fieldReference);
		}

		@SuppressWarnings("NullAway")
		private ArrayElemAt createArrayElemAt() {

			if (usesFieldRef()) {
				return ArrayElemAt.arrayOf(fieldReference);
			}

			return usesExpression() ? ArrayElemAt.arrayOf(expression) : ArrayElemAt.arrayOf(values);
		}

		/**
		 * 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 new instance of {@link ConcatArrays}.
		 */
		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 new instance of {@link ConcatArrays}.
		 */
		public ConcatArrays concat(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null");
			return createConcatArrays().concat(expression);
		}

		@SuppressWarnings("NullAway")
		private ConcatArrays createConcatArrays() {

			if (usesFieldRef()) {
				return ConcatArrays.arrayOf(fieldReference);
			}

			return usesExpression() ? ConcatArrays.arrayOf(expression) : ConcatArrays.arrayOf(values);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes the associated array and selects a subset of the array to
		 * return based on the specified condition.
		 *
		 * @return new instance of {@link AsBuilder} to create a {@link Filter}.
		 */
		@SuppressWarnings("NullAway")
		public AsBuilder filter() {

			if (usesFieldRef()) {
				return Filter.filter(fieldReference);
			}

			if (usesExpression()) {
				return Filter.filter(expression);
			}

			Assert.state(values != null, "Values must not be null");
			return Filter.filter(new ArrayList<>(values));
		}

		/**
		 * Creates new {@link AggregationExpression} that takes the associated array and an check if its an array.
		 *
		 * @return new instance of {@link IsArray}.
		 */
		@SuppressWarnings("NullAway")
		public IsArray isArray() {

			Assert.state(values == null, "Does it make sense to call isArray on an array; Maybe just skip it");

			return usesFieldRef() ? IsArray.isArray(fieldReference) : IsArray.isArray(expression);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes the associated array and retrieves its length.
		 *
		 * @return new instance of {@link Size}.
		 */
		@SuppressWarnings("NullAway")
		public Size length() {

			if (usesFieldRef()) {
				return Size.lengthOfArray(fieldReference);
			}

			return usesExpression() ? Size.lengthOfArray(expression) : Size.lengthOfArray(values);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes the associated array and selects a subset from it.
		 *
		 * @return new instance of {@link Slice}.
		 */
		@SuppressWarnings("NullAway")
		public Slice slice() {

			if (usesFieldRef()) {
				return Slice.sliceArrayOf(fieldReference);
			}

			return usesExpression() ? Slice.sliceArrayOf(expression) : Slice.sliceArrayOf(values);
		}

		/**
		 * 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 new instance of {@link IndexOfArray}.
		 */
		@SuppressWarnings("NullAway")
		public IndexOfArray indexOf(Object value) {

			if (usesFieldRef()) {
				return IndexOfArray.arrayOf(fieldReference).indexOf(value);
			}

			return usesExpression() ? IndexOfArray.arrayOf(expression).indexOf(value)
					: IndexOfArray.arrayOf(values).indexOf(value);
		}

		/**
		 * Creates new {@link AggregationExpression} that returns an array with the elements in reverse order.
		 *
		 * @return new instance of {@link ReverseArray}.
		 */
		@SuppressWarnings("NullAway")
		public ReverseArray reverse() {

			if (usesFieldRef()) {
				return ReverseArray.reverseArrayOf(fieldReference);
			}

			return usesExpression() ? ReverseArray.reverseArrayOf(expression)
					: ReverseArray.reverseArrayOf(Collections.singletonList(values));
		}

		/**
		 * 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 new instance of {@link ReduceInitialValueBuilder} to create {@link Reduce}.
		 */
		@SuppressWarnings("NullAway")
		public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(AggregationExpression expression) {

			return initialValue -> (usesFieldRef() ? Reduce.arrayOf(fieldReference)
					: Reduce.arrayOf(ArrayOperatorFactory.this.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 must not be {@literal null}.
		 * @return new instance of {@link ReduceInitialValueBuilder} to create {@link Reduce}.
		 */
		@SuppressWarnings("NullAway")
		public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(PropertyExpression... expressions) {

			return initialValue -> (usesFieldRef() ? Reduce.arrayOf(fieldReference) : Reduce.arrayOf(expression))
					.withInitialValue(initialValue).reduce(expressions);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes the associated array and sorts it by the given {@link Sort
		 * order}.
		 *
		 * @return new instance of {@link SortArray}.
		 * @since 4.0
		 */
		@SuppressWarnings("NullAway")
		public SortArray sort(Sort sort) {

			if (usesFieldRef()) {
				return SortArray.sortArrayOf(fieldReference).by(sort);
			}

			return (usesExpression() ? SortArray.sortArrayOf(expression) : SortArray.sortArray(values)).by(sort);
		}

		/**
		 * Creates new {@link AggregationExpression} that takes the associated array and sorts it by the given
		 * {@link Direction order}.
		 *
		 * @return new instance of {@link SortArray}.
		 * @since 4.5
		 */
		@SuppressWarnings("NullAway")
		public SortArray sort(Direction direction) {

			if (usesFieldRef()) {
				return SortArray.sortArrayOf(fieldReference).direction(direction);
			}

			return (usesExpression() ? SortArray.sortArrayOf(expression) : SortArray.sortArray(values)).direction(direction);
		}

		/**
		 * 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 new instance of {@link Zip}.
		 */
		@SuppressWarnings("NullAway")
		public Zip zipWith(Object... arrays) {

			if (usesFieldRef()) {
				return Zip.arrayOf(fieldReference).zip(arrays);
			}

			return (usesExpression() ? Zip.arrayOf(expression) : Zip.arrayOf(values)).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 new instance of {@link In}.
		 */
		@SuppressWarnings("NullAway")
		public In containsValue(Object value) {

			if (usesFieldRef()) {
				return In.arrayOf(fieldReference).containsValue(value);
			}

			return (usesExpression() ? In.arrayOf(expression) : In.arrayOf(values)).containsValue(value);
		}

		/**
		 * Creates new {@link AggregationExpression} that converts the associated expression into an object.
		 * <strong>NOTE:</strong> Requires MongoDB 3.6 or later.
		 *
		 * @return new instance of {@link ArrayToObject}.
		 * @since 2.1
		 */
		@SuppressWarnings("NullAway")
		public ArrayToObject toObject() {

			if (usesFieldRef()) {
				return ArrayToObject.arrayValueOfToObject(fieldReference);
			}

			return usesExpression() ? ArrayToObject.arrayValueOfToObject(expression) : ArrayToObject.arrayToObject(values);
		}

		/**
		 * Creates new {@link AggregationExpression} that return the first element in the associated array.
		 * <strong>NOTE:</strong> Requires MongoDB 4.4 or later.
		 *
		 * @return new instance of {@link First}.
		 * @since 3.4
		 */
		@SuppressWarnings("NullAway")
		public First first() {

			if (usesFieldRef()) {
				return First.firstOf(fieldReference);
			}

			return usesExpression() ? First.firstOf(expression) : First.first(values);
		}

		/**
		 * Creates new {@link AggregationExpression} that return the last element in the given array. <strong>NOTE:</strong>
		 * Requires MongoDB 4.4 or later.
		 *
		 * @return new instance of {@link Last}.
		 * @since 3.4
		 */
		@SuppressWarnings("NullAway")
		public Last last() {

			if (usesFieldRef()) {
				return Last.lastOf(fieldReference);
			}

			return usesExpression() ? Last.lastOf(expression) : Last.last(values);
		}

		/**
		 * @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);
		}

		/**
		 * @return {@literal true} if {@link #fieldReference} is not {@literal null}.
		 */
		private boolean usesFieldRef() {
			return fieldReference != null;
		}

		/**
		 * @return {@literal true} if {@link #expression} is not {@literal null}.
		 * @since 2.2
		 */
		private boolean usesExpression() {
			return expression != 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 new instance of {@link ArrayElemAt}.
		 */
		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 new instance of {@link ArrayElemAt}.
		 */
		public static ArrayElemAt arrayOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null");
			return new ArrayElemAt(Collections.singletonList(expression));
		}

		/**
		 * Creates new {@link ArrayElemAt}.
		 *
		 * @param values The array members. Must not be {@literal null}.
		 * @return new instance of {@link ArrayElemAt}.
		 * @since 2.2
		 */
		public static ArrayElemAt arrayOf(Collection<?> values) {

			Assert.notNull(values, "Values must not be null");
			return new ArrayElemAt(Collections.singletonList(values));
		}

		/**
		 * Use the element with given index number.
		 *
		 * @param index the index number
		 * @return new instance of {@link ArrayElemAt}.
		 */
		@Contract("_ -> new")
		public ArrayElemAt elementAt(int index) {
			return new ArrayElemAt(append(index));
		}

		/**
		 * Use the element at the index number evaluated from the given {@link AggregationExpression}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return new instance of {@link ArrayElemAt}.
		 */
		@Contract("_ -> new")
		public ArrayElemAt elementAt(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null");
			return new ArrayElemAt(append(expression));
		}

		/**
		 * Use the element at the index number taken from the given field.
		 *
		 * @param arrayFieldReference the field name.
		 * @return new instance of {@link ArrayElemAt}.
		 */
		@Contract("_ -> new")
		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 new instance of {@link ConcatArrays}.
		 */
		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 new instance of {@link ConcatArrays}.
		 */
		public static ConcatArrays arrayOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null");
			return new ConcatArrays(Collections.singletonList(expression));
		}

		/**
		 * Creates new {@link ConcatArrays}.
		 *
		 * @param values The array members. Must not be {@literal null}.
		 * @return new instance of {@link ConcatArrays}.
		 * @since 2.2
		 */
		public static ConcatArrays arrayOf(Collection<?> values) {

			Assert.notNull(values, "Values must not be null");
			return new ConcatArrays(Collections.singletonList(values));
		}

		/**
		 * Concat with the array stored at the given field.
		 *
		 * @param arrayFieldReference must not be {@literal null}.
		 * @return new instance of {@link ConcatArrays}.
		 */
		@Contract("_ -> new")
		public ConcatArrays concat(String arrayFieldReference) {

			Assert.notNull(arrayFieldReference, "ArrayFieldReference must not be null");
			return new ConcatArrays(append(Fields.field(arrayFieldReference)));
		}

		/**
		 * Concat with the array resulting from the given {@link AggregationExpression}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return new instance of {@link ConcatArrays}.
		 */
		@Contract("_ -> new")
		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 @Nullable Object input;
		private @Nullable ExposedField as;
		private @Nullable 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 {@link AggregationExpression} resolving to an array to apply the {@code $filter} to.
		 *
		 * @param expression must not be {@literal null}.
		 * @return never {@literal null}.
		 * @since 4.2
		 */
		public static AsBuilder filter(AggregationExpression expression) {

			Assert.notNull(expression, "Field must not be null");
			return new FilterExpressionBuilder().filter(expression);
		}

		/**
		 * Set the {@literal values} to apply the {@code $filter} to.
		 *
		 * @param values must not be {@literal null}.
		 * @return new instance of {@link AsBuilder} to create the {@link Filter}.
		 */
		public static AsBuilder filter(List<?> values) {

			Assert.notNull(values, "Values must not be null");
			return new FilterExpressionBuilder().filter(values);
		}

		@Override
		public Document toDocument(final AggregationOperationContext context) {

			Assert.notNull(as, "As must be set first");
			return toFilter(ExposedFields.from(as), context);
		}

		@SuppressWarnings("NullAway")
		private Document toFilter(ExposedFields exposedFields, AggregationOperationContext context) {

			Document filterExpression = new Document();
			AggregationOperationContext operationContext = context.inheritAndExpose(exposedFields);

			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 @Nullable Object getMappedInput(AggregationOperationContext context) {

			if (input instanceof Field field) {
				return context.getReference(field).toString();
			}

			if (input instanceof AggregationExpression expression) {
				return expression.toDocument(context);
			}

			return input;
		}

		private @Nullable Object getMappedCondition(AggregationOperationContext context) {

			if (!(condition instanceof AggregationExpression aggregationExpression)) {
				return condition;
			}

			NestedDelegatingExpressionAggregationOperationContext nea = new NestedDelegatingExpressionAggregationOperationContext(
					context, Collections.singleton(as));
			return aggregationExpression.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);

			/**
			 * Set the {@link AggregationExpression} resolving to an array to apply the {@code $filter} to.
			 *
			 * @param expression must not be {@literal null}.
			 * @return
			 * @since 4.1.1
			 */
			AsBuilder filter(AggregationExpression expression);
		}

		/**
		 * @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 never {@literal null}.
			 */
			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 never {@literal null}.
			 */
			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 never {@literal null}.
			 */
			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 never {@literal null}.
			 */
			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 new instance of {@link FilterExpressionBuilder}.
			 */
			public static InputBuilder newBuilder() {
				return new FilterExpressionBuilder();
			}

			@Override
			@Contract("_ -> this")
			public AsBuilder filter(List<?> array) {

				Assert.notNull(array, "Array must not be null");
				filter.input = new ArrayList<>(array);
				return this;
			}

			@Override
			@Contract("_ -> this")
			public AsBuilder filter(Field field) {

				Assert.notNull(field, "Field must not be null");
				filter.input = field;
				return this;
			}

			@Override
			@Contract("_ -> this")
			public AsBuilder filter(AggregationExpression expression) {

				Assert.notNull(expression, "Expression must not be null");
				filter.input = expression;
				return this;
			}

			@Override
			@Contract("_ -> this")
			public ConditionBuilder as(String variableName) {

				Assert.notNull(variableName, "Variable name  must not be null");
				filter.as = new ExposedField(variableName, true);
				return this;
			}

			@Override
			public Filter by(AggregationExpression condition) {

				Assert.notNull(condition, "Condition must not be null");
				filter.condition = condition;
				return filter;
			}

			@Override
			public Filter by(String expression) {

				Assert.notNull(expression, "Expression must not be null");
				filter.condition = expression;
				return filter;
			}

			@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 new instance of {@link IsArray}.
		 */
		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 new instance of {@link IsArray}.
		 */
		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 new instance of {@link Size}.
		 */
		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 new instance of {@link Size}.
		 */
		public static Size lengthOfArray(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null");
			return new Size(expression);
		}

		/**
		 * Creates new {@link Size}.
		 *
		 * @param values must not be {@literal null}.
		 * @return new instance of {@link Size}.
		 * @since 2.2
		 */
		public static Size lengthOfArray(Collection<?> values) {

			Assert.notNull(values, "Values must not be null");
			return new Size(Collections.singletonList(values));
		}
	}

	/**
	 * {@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 new instance of {@link Slice}.
		 */
		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 new instance of {@link Slice}.
		 */
		public static Slice sliceArrayOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null");
			return new Slice(Collections.singletonList(expression));
		}

		/**
		 * Creates new {@link Slice}.
		 *
		 * @param values must not be {@literal null}.
		 * @return new instance of {@link Slice}.
		 * @since 2.2
		 */
		public static Slice sliceArrayOf(Collection<?> values) {

			Assert.notNull(values, "Values must not be null");
			return new Slice(Collections.singletonList(values));
		}

		/**
		 * Slice the number of elements.
		 *
		 * @param count number of elements to slice.
		 * @return new instance of {@link Slice}.
		 */
		@Contract("_ -> new")
		public Slice itemCount(int count) {
			return new Slice(append(count));
		}

		/**
		 * Slice the number of elements.
		 *
		 * @param count an {@link AggregationExpression} that evaluates to a numeric value used as number of elements to
		 *          slice.
		 * @return new instance of {@link Slice}.
		 * @since 4.5
		 */
		@Contract("_ -> new")
		public Slice itemCount(AggregationExpression count) {
			return new Slice(append(count));
		}

		/**
		 * Slice using offset and count.
		 *
		 * @param position the start position
		 * @return new instance of {@link SliceElementsBuilder} to create {@link Slice}.
		 */
		public SliceElementsBuilder offset(int position) {
			return new SliceElementsBuilder(position);
		}

		/**
		 * Slice using offset and count.
		 *
		 * @param position the start position
		 * @return new instance of {@link SliceElementsBuilder} to create {@link Slice}.
		 */
		public SliceElementsBuilder offset(AggregationExpression position) {
			return new SliceElementsBuilder(position);
		}

		/**
		 * @author Christoph Strobl
		 */
		public class SliceElementsBuilder {

			private final Object position;

			SliceElementsBuilder(Object position) {
				this.position = position;
			}

			/**
			 * Set the number of elements given {@literal count}.
			 *
			 * @param count number of elements to slice.
			 * @return new instance of {@link Slice}.
			 */
			public Slice itemCount(int count) {
				return new Slice(append(position)).itemCount(count);
			}

			/**
			 * Slice the number of elements.
			 *
			 * @param count an {@link AggregationExpression} that evaluates to a numeric value used as number of elements to
			 *          slice.
			 * @return new instance of {@link Slice}.
			 * @since 4.5
			 */
			public Slice itemCount(AggregationExpression count) {
				return new Slice(append(position)).itemCount(count);
			}
		}
	}

	/**
	 * {@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 new instance of {@link IndexOfArray}.
		 */
		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 new instance of {@link IndexOfArray}.
		 */
		public static IndexOfArrayBuilder arrayOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null");
			return new IndexOfArrayBuilder(expression);
		}

		/**
		 * Start creating new {@link IndexOfArray}.
		 *
		 * @param values must not be {@literal null}.
		 * @return new instance of {@link IndexOfArrayBuilder} to create {@link IndexOfArray}.
		 * @since 2.2
		 */
		public static IndexOfArrayBuilder arrayOf(Collection<?> values) {

			Assert.notNull(values, "Values must not be null");
			return new IndexOfArrayBuilder(values);
		}

		/**
		 * Lookup within a given range.
		 *
		 * @param range the lookup range.
		 * @return new instance of {@link IndexOfArray}.
		 */
		@Contract("_ -> new")
		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 new instance of {@link IndexOfArray}.
			 */
			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 new instance of {@link RangeOperatorBuilder} to create {@link RangeOperator}.
		 */
		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 new instance of {@link RangeOperatorBuilder} to create {@link RangeOperator}.
		 */
		public static RangeOperatorBuilder rangeStartingAt(AggregationExpression expression) {
			return new RangeOperatorBuilder(expression);
		}

		/**
		 * Start creating new {@link RangeOperator}.
		 *
		 * @param value
		 * @return new instance of {@link RangeOperator}.
		 */
		public static RangeOperatorBuilder rangeStartingAt(long value) {
			return new RangeOperatorBuilder(value);
		}

		@Contract("_ -> new")
		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 new instance of {@link RangeOperator}.
			 */
			public RangeOperator to(long index) {
				return new RangeOperator(Arrays.asList(startPoint, index));
			}

			/**
			 * Creates new {@link RangeOperator}.
			 *
			 * @param expression must not be {@literal null}.
			 * @return new instance of {@link RangeOperator}.
			 */
			public RangeOperator to(AggregationExpression expression) {
				return new RangeOperator(Arrays.asList(startPoint, expression));
			}

			/**
			 * Creates new {@link RangeOperator}.
			 *
			 * @param fieldReference must not be {@literal null}.
			 * @return new instance of {@link RangeOperator}.
			 */
			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 new instance of {@link ReverseArray}.
		 */
		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 new instance of {@link ReverseArray}.
		 */
		public static ReverseArray reverseArrayOf(AggregationExpression expression) {
			return new ReverseArray(expression);
		}

		/**
		 * Creates new {@link ReverseArray}.
		 *
		 * @param values must not be {@literal null}.
		 * @return new instance of {@link ReverseArray}.
		 * @since 2.2
		 */
		public static ReverseArray reverseArrayOf(Collection<?> values) {
			return new ReverseArray(values);
		}
	}

	/**
	 * {@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;
		}

		@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);
		}

		@SuppressWarnings("NullAway")
		private Object getMappedValue(Object value, AggregationOperationContext context) {

			if (value instanceof Document) {
				return value;
			}
			if (value instanceof AggregationExpression aggregationExpression) {
				return aggregationExpression.toDocument(context);
			} else if (value instanceof Field field) {
				return context.getReference(field).toString();
			} else {
				return context.getMappedObject(new Document("###val###", value)).get("###val###");
			}
		}

		/**
		 * Start creating new {@link Reduce}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return new instance of {@link InitialValueBuilder} to create {@link Reduce}.
		 */
		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 arrayValueExpression must not be {@literal null}.
		 * @return new instance of {@link InitialValueBuilder} to create {@link Reduce}.
		 */
		public static InitialValueBuilder arrayOf(final AggregationExpression arrayValueExpression) {

			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(arrayValueExpression, initialValue, Collections.singletonList(expression));
						}

						@Override
						public Reduce reduce(PropertyExpression... expressions) {

							Assert.notNull(expressions, "PropertyExpressions must not be null");
							return new Reduce(arrayValueExpression, initialValue, Arrays.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 never {@literal null}.
			 */
			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 new instance of {@link Reduce}.
			 */
			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 new instance of {@link Reduce}.
			 */
			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 new instance of {@link AsBuilder} to create {@link Reduce}.
			 */
			public static AsBuilder property(final String name) {

				return new AsBuilder() {

					@Override
					public PropertyExpression definedAs(AggregationExpression expression) {
						return new PropertyExpression(name, expression);
					}
				};
			}

			@Override
			public Document toDocument(AggregationOperationContext context) {
				return new Document(propertyName, aggregationExpression.toDocument(context));
			}

			/**
			 * @author Christoph Strobl
			 */
			public interface AsBuilder {

				/**
				 * Set the {@link AggregationExpression} resulting in the properties value.
				 *
				 * @param expression must not be {@literal null}.
				 * @return never {@literal null}.
				 */
				PropertyExpression definedAs(AggregationExpression expression);
			}
		}

		public enum Variable implements AggregationVariable {

			THIS {

				@Override
				public String getTarget() {
					return "$$this";
				}

				@Override
				public String toString() {
					return getName();
				}
			},

			VALUE {

				@Override
				public String getTarget() {
					return "$$value";
				}

				@Override
				public String toString() {
					return getName();
				}
			};

			@Override
			public boolean isInternal() {
				return true;
			}

			/**
			 * 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 never {@literal null}.
			 */
			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();
					}
				};
			}

			public static boolean isVariable(Field field) {

				for (Variable var : values()) {
					if (field.getTarget().startsWith(var.getTarget())) {
						return true;
					}
				}
				return false;
			}
		}
	}

	/**
	 * {@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 new instance of {@link ZipBuilder} to create {@link Zip}.
		 */
		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 new instance of {@link ZipBuilder} to create {@link Zip}.
		 */
		public static ZipBuilder arrayOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null");
			return new ZipBuilder(expression);
		}

		/**
		 * Start creating new {@link Zip}.
		 *
		 * @param values must not be {@literal null}.
		 * @return new instance of {@link Zip}.
		 * @since 2.2
		 */
		public static ZipBuilder arrayOf(Collection<?> values) {

			Assert.notNull(values, "Expression must not be null");
			return new ZipBuilder(values);
		}

		/**
		 * Create new {@link Zip} and set the {@code useLongestLength} property to {@literal true}.
		 *
		 * @return new instance of {@link Zip}.
		 */
		@Contract("-> new")
		public Zip useLongestLength() {
			return new Zip(append("useLongestLength", true));
		}

		/**
		 * Optionally provide a default value.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return new instance of {@link Zip}.
		 */
		@Contract("_ -> new")
		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 new instance of {@link Zip}.
		 */
		@Contract("_ -> new")
		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 new instance of {@link Zip}.
		 */
		@Contract("_ -> new")
		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<>();
				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 new instance of {@link Zip}.
			 */
			public Zip zip(Object... arrays) {

				Assert.notNull(arrays, "Arrays must not be null");
				for (Object value : arrays) {

					if (value instanceof String stringValue) {
						sourceArrays.add(Fields.field(stringValue));
					} else {
						sourceArrays.add(value);
					}
				}

				return new Zip(Collections.singletonMap("inputs", sourceArrays));
			}
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $in}.
	 *
	 * @author Christoph Strobl
	 * @author Shashank Sharma
	 * @see <a href=
	 *      "https://docs.mongodb.com/manual/reference/operator/aggregation/in/">https://docs.mongodb.com/manual/reference/operator/aggregation/in/</a>
	 * @since 2.2
	 */
	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 new instance of {@link InBuilder} to create {@link In}.
		 */
		public static InBuilder arrayOf(String fieldReference) {

			Assert.notNull(fieldReference, "FieldReference must not be null");

			return 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 new instance of {@link InBuilder} to create {@link In}.
		 */
		public static InBuilder arrayOf(AggregationExpression expression) {

			Assert.notNull(expression, "Expression must not be null");

			return value -> {

				Assert.notNull(value, "Value must not be null");

				return new In(Arrays.asList(value, expression));
			};
		}

		/**
		 * Support for Aggregation In Search an Element in List of Objects to Filter Start creating {@link In}.
		 *
		 * @param values must not be {@literal null}.
		 * @return new instance of {@link InBuilder}.
		 * @since 2.2
		 */
		public static InBuilder arrayOf(Collection<?> values) {

			Assert.notNull(values, "Values must not be null");

			return value -> {

				Assert.notNull(value, "Value must not be null");

				return new In(Arrays.asList(value, values));
			};
		}

		/**
		 * @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 new instance of {@link In}.
			 */
			In containsValue(Object value);
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $arrayToObject} that transforms an array into a single document. <br />
	 * <strong>NOTE:</strong> Requires MongoDB 3.6 or later.
	 *
	 * @author Christoph Strobl
	 * @see <a href=
	 *      "https://docs.mongodb.com/manual/reference/operator/aggregation/arrayToObject/">https://docs.mongodb.com/manual/reference/operator/aggregation/arrayToObject/</a>
	 * @since 2.1
	 */
	public static class ArrayToObject extends AbstractAggregationExpression {

		private ArrayToObject(Object value) {
			super(value);
		}

		/**
		 * Converts the given array (e.g. an array of two-element arrays, a field reference to an array,...) to an object.
		 *
		 * @param array must not be {@literal null}.
		 * @return new instance of {@link ArrayToObject}.
		 */
		public static ArrayToObject arrayToObject(Object array) {
			return new ArrayToObject(array);
		}

		/**
		 * Converts the array pointed to by the given {@link Field field reference} to an object.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return new instance of {@link ArrayToObject}.
		 */
		public static ArrayToObject arrayValueOfToObject(String fieldReference) {
			return new ArrayToObject(Fields.field(fieldReference));
		}

		/**
		 * Converts the result array of the given {@link AggregationExpression expression} to an object.
		 *
		 * @param expression must not be {@literal null}.
		 * @return new instance of {@link ArrayToObject}.
		 */
		public static ArrayToObject arrayValueOfToObject(AggregationExpression expression) {
			return new ArrayToObject(expression);
		}

		@Override
		protected String getMongoMethod() {
			return "$arrayToObject";
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $first} that returns the first element in an array. <br />
	 * <strong>NOTE:</strong> Requires MongoDB 4.4 or later.
	 *
	 * @author Divya Srivastava
	 * @author Christoph Strobl
	 * @since 3.4
	 */
	public static class First extends AbstractAggregationExpression {

		private First(Object value) {
			super(value);
		}

		/**
		 * Returns the first element in the given array.
		 *
		 * @param array must not be {@literal null}.
		 * @return new instance of {@link First}.
		 */
		public static First first(Object array) {
			return new First(array);
		}

		/**
		 * Returns the first element in the array pointed to by the given {@link Field field reference}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return new instance of {@link First}.
		 */
		public static First firstOf(String fieldReference) {
			return new First(Fields.field(fieldReference));
		}

		/**
		 * Returns the first element of the array computed by the given {@link AggregationExpression expression}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return new instance of {@link First}.
		 */
		public static First firstOf(AggregationExpression expression) {
			return new First(expression);
		}

		@Override
		protected String getMongoMethod() {
			return "$first";
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $last} that returns the last element in an array. <br />
	 * <strong>NOTE:</strong> Requires MongoDB 4.4 or later.
	 *
	 * @author Divya Srivastava
	 * @author Christoph Strobl
	 * @since 3.4
	 */
	public static class Last extends AbstractAggregationExpression {

		private Last(Object value) {
			super(value);
		}

		/**
		 * Returns the last element in the given array.
		 *
		 * @param array must not be {@literal null}.
		 * @return new instance of {@link Last}.
		 */
		public static Last last(Object array) {
			return new Last(array);
		}

		/**
		 * Returns the last element in the array pointed to by the given {@link Field field reference}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return new instance of {@link Last}.
		 */
		public static Last lastOf(String fieldReference) {
			return new Last(Fields.field(fieldReference));
		}

		/**
		 * Returns the last element of the array computed buy the given {@link AggregationExpression expression}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return new instance of {@link Last}.
		 */
		public static Last lastOf(AggregationExpression expression) {
			return new Last(expression);
		}

		@Override
		protected String getMongoMethod() {
			return "$last";
		}
	}

	/**
	 * {@link AggregationExpression} for {@code $sortArray} that sorts elements in an array. <br />
	 *
	 * @author Christoph Strobl
	 * @since 4.0
	 */
	public static class SortArray extends AbstractAggregationExpression {

		private SortArray(Object value) {
			super(value);
		}

		/**
		 * Returns the given array.
		 *
		 * @param array must not be {@literal null}.
		 * @return new instance of {@link SortArray}.
		 */
		public static SortArray sortArray(Object array) {
			return new SortArray(Collections.singletonMap("input", array));
		}

		/**
		 * Sorts the elements in the array pointed to by the given {@link Field field reference}.
		 *
		 * @param fieldReference must not be {@literal null}.
		 * @return new instance of {@link SortArray}.
		 */
		public static SortArray sortArrayOf(String fieldReference) {
			return sortArray(Fields.field(fieldReference));
		}

		/**
		 * Sorts the elements of the array computed buy the given {@link AggregationExpression expression}.
		 *
		 * @param expression must not be {@literal null}.
		 * @return new instance of {@link SortArray}.
		 */
		public static SortArray sortArrayOf(AggregationExpression expression) {
			return sortArray(expression);
		}

		/**
		 * Set the order to put elements in.
		 *
		 * @param sort must not be {@literal null}.
		 * @return new instance of {@link SortArray}.
		 */
		@Contract("_ -> new")
		public SortArray by(Sort sort) {
			return new SortArray(append("sortBy", sort));
		}

		/**
		 * Order the values for the array in the given direction.
		 *
		 * @param direction must not be {@literal null}.
		 * @return new instance of {@link SortArray}.
		 * @since 4.5
		 */
		public SortArray direction(Direction direction) {
			return new SortArray(append("sortBy", direction.isAscending() ? 1 : -1));
		}

		/**
		 * Sort the array elements by their values in ascending order. Suitable for arrays of simple types (e.g., integers,
		 * strings).
		 *
		 * @return new instance of {@link SortArray}.
		 * @since 4.5
		 */
		public SortArray byValueAscending() {
			return direction(Direction.ASC);
		}

		/**
		 * Sort the array elements by their values in descending order. Suitable for arrays of simple types (e.g., integers,
		 * strings).
		 *
		 * @return new instance of {@link SortArray}.
		 * @since 4.5
		 */
		public SortArray byValueDescending() {
			return direction(Direction.DESC);
		}

		@Override
		protected String getMongoMethod() {
			return "$sortArray";
		}
	}
}