diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
index b1ee37b8a..1cdf93fb9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
@@ -202,7 +202,7 @@ public class Aggregation {
}
/**
- * Creates a new {@link ProjectionOperation} includeing the given {@link Fields}.
+ * Creates a new {@link ProjectionOperation} including the given {@link Fields}.
*
* @param fields must not be {@literal null}.
* @return
@@ -418,6 +418,26 @@ public class Aggregation {
return new OutOperation(outCollectionName);
}
+ /**
+ * Creates a new {@link BucketOperation} using given {@literal groupByField}.
+ *
+ * @param groupByField must not be {@literal null} or empty.
+ * @return
+ */
+ public static BucketOperation bucket(String groupByField) {
+ return new BucketOperation(field(groupByField));
+ }
+
+ /**
+ * Creates a new {@link BucketOperation} using given {@link AggregationExpression group-by expression}.
+ *
+ * @param groupByExpression must not be {@literal null}.
+ * @return
+ */
+ public static BucketOperation bucket(AggregationExpression groupByExpression) {
+ return new BucketOperation(groupByExpression);
+ }
+
/**
* Creates a new {@link LookupOperation}.
*
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperation.java
new file mode 100644
index 000000000..080ece369
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperation.java
@@ -0,0 +1,226 @@
+/*
+ * 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.springframework.data.mongodb.core.aggregation.BucketOperation.BucketOperationOutputBuilder;
+import org.springframework.util.Assert;
+
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBObject;
+
+/**
+ * Encapsulates the aggregation framework {@code $bucket}-operation.
+ *
+ * Bucket stage is typically used with {@link Aggregation} and {@code $facet}. Categorizes incoming documents into
+ * groups, called buckets, based on a specified expression and bucket boundaries.
+ *
+ * We recommend to use the static factory method {@link Aggregation#bucket(String)} instead of creating instances of
+ * this class directly.
+ *
+ * @see http://docs.mongodb.org/manual/reference/aggregation/bucket/
+ * @see BucketOperationSupport
+ * @author Mark Paluch
+ * @since 1.10
+ */
+public class BucketOperation extends BucketOperationSupport
+ implements FieldsExposingAggregationOperation {
+
+ private final List boundaries;
+ private final Object defaultBucket;
+
+ /**
+ * Creates a new {@link BucketOperation} given a {@link Field group-by field}.
+ *
+ * @param groupByField must not be {@literal null}.
+ */
+ public BucketOperation(Field groupByField) {
+
+ super(groupByField);
+
+ this.boundaries = Collections.emptyList();
+ this.defaultBucket = null;
+ }
+
+ /**
+ * Creates a new {@link BucketOperation} given a {@link AggregationExpression group-by expression}.
+ *
+ * @param groupByExpression must not be {@literal null}.
+ */
+ public BucketOperation(AggregationExpression groupByExpression) {
+
+ super(groupByExpression);
+
+ this.boundaries = Collections.emptyList();
+ this.defaultBucket = null;
+ }
+
+ private BucketOperation(BucketOperation bucketOperation, Outputs outputs) {
+
+ super(bucketOperation, outputs);
+
+ this.boundaries = bucketOperation.boundaries;
+ this.defaultBucket = bucketOperation.defaultBucket;
+ }
+
+ private BucketOperation(BucketOperation bucketOperation, List boundaries, Object defaultBucket) {
+
+ super(bucketOperation);
+
+ this.boundaries = new ArrayList(boundaries);
+ this.defaultBucket = defaultBucket;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
+ */
+ @Override
+ public DBObject toDBObject(AggregationOperationContext context) {
+
+ DBObject options = new BasicDBObject();
+
+ options.put("boundaries", context.getMappedObject(new BasicDBObject("$set", boundaries)).get("$set"));
+
+ if (defaultBucket != null) {
+ options.put("default", context.getMappedObject(new BasicDBObject("$set", defaultBucket)).get("$set"));
+ }
+
+ options.putAll(super.toDBObject(context));
+
+ return new BasicDBObject("$bucket", options);
+ }
+
+ /**
+ * Configures a default bucket {@literal literal} and return a new {@link BucketOperation}.
+ *
+ * @param literal must not be {@literal null}.
+ * @return
+ */
+ public BucketOperation withDefaultBucket(Object literal) {
+
+ Assert.notNull(literal, "Default bucket literal must not be null!");
+ return new BucketOperation(this, boundaries, literal);
+ }
+
+ /**
+ * Configures {@literal boundaries} and return a new {@link BucketOperation}. Existing {@literal boundaries} are
+ * preserved and the new {@literal boundaries} are appended.
+ *
+ * @param boundaries must not be {@literal null}.
+ * @return
+ */
+ public BucketOperation withBoundaries(Object... boundaries) {
+
+ Assert.notNull(boundaries, "Boundaries must not be null!");
+
+ List newBoundaries = new ArrayList(this.boundaries.size() + boundaries.length);
+ newBoundaries.addAll(this.boundaries);
+ newBoundaries.addAll(Arrays.asList(boundaries));
+
+ return new BucketOperation(this, newBoundaries, defaultBucket);
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#newBucketOperation(org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.Outputs)
+ */
+ @Override
+ protected BucketOperation newBucketOperation(Outputs outputs) {
+ return new BucketOperation(this, outputs);
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#andOutputExpression(java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public ExpressionBucketOperationBuilder andOutputExpression(String expression, Object... params) {
+ return new ExpressionBucketOperationBuilder(expression, this, params);
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#andOutput(org.springframework.data.mongodb.core.aggregation.AggregationExpression)
+ */
+ @Override
+ public BucketOperationOutputBuilder andOutput(AggregationExpression expression) {
+ return new BucketOperationOutputBuilder(expression, this);
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#andOutput(java.lang.String)
+ */
+ @Override
+ public BucketOperationOutputBuilder andOutput(String fieldName) {
+ return new BucketOperationOutputBuilder(Fields.field(fieldName), this);
+ }
+
+ /**
+ * {@link OutputBuilder} implementation for {@link BucketOperation}.
+ */
+ public static class BucketOperationOutputBuilder
+ extends BucketOperationSupport.OutputBuilder {
+
+ /**
+ * Creates a new {@link BucketOperationOutputBuilder} fot the given value and {@link BucketOperation}.
+ *
+ * @param value must not be {@literal null}.
+ * @param operation must not be {@literal null}.
+ */
+ protected BucketOperationOutputBuilder(Object value, BucketOperation operation) {
+ super(value, operation);
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder#apply(org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OperationOutput)
+ */
+ @Override
+ protected BucketOperationOutputBuilder apply(OperationOutput operationOutput) {
+ return new BucketOperationOutputBuilder(operationOutput, this.operation);
+ }
+ }
+
+ /**
+ * {@link ExpressionBucketOperationBuilderSupport} implementation for {@link BucketOperation} using SpEL expression
+ * based {@link Output}.
+ *
+ * @author Mark Paluch
+ */
+ public static class ExpressionBucketOperationBuilder
+ extends ExpressionBucketOperationBuilderSupport {
+
+ /**
+ * Creates a new {@link ExpressionBucketOperationBuilderSupport} for the given value, {@link BucketOperation}
+ * and parameters.
+ *
+ * @param expression must not be {@literal null}.
+ * @param operation must not be {@literal null}.
+ * @param parameters
+ */
+ protected ExpressionBucketOperationBuilder(String expression, BucketOperation operation, Object[] parameters) {
+ super(expression, operation, parameters);
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder#apply(org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OperationOutput)
+ */
+ @Override
+ protected BucketOperationOutputBuilder apply(OperationOutput operationOutput) {
+ return new BucketOperationOutputBuilder(operationOutput, this.operation);
+ }
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java
new file mode 100644
index 000000000..13a63e6bd
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java
@@ -0,0 +1,697 @@
+/*
+ * 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.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder;
+import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
+import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder;
+import org.springframework.expression.spel.ast.Projection;
+import org.springframework.util.Assert;
+
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBObject;
+
+/**
+ * Base class for bucket operations that support output expressions the aggregation framework.
+ *
+ * Bucket stages collect documents into buckets and can contribute output fields.
+ *
+ * Implementing classes are required to provide an {@link OutputBuilder}.
+ *
+ * @see http://docs.mongodb.org/manual/reference/aggregation/bucket/
+ * @author Mark Paluch
+ * @since 1.10
+ */
+public abstract class BucketOperationSupport, B extends OutputBuilder>
+ implements FieldsExposingAggregationOperation {
+
+ private final Field groupByField;
+ private final AggregationExpression groupByExpression;
+ private final Outputs outputs;
+
+ /**
+ * Creates a new {@link BucketOperationSupport} given a {@link Field group-by field}.
+ *
+ * @param groupByField must not be {@literal null}.
+ */
+ protected BucketOperationSupport(Field groupByField) {
+
+ Assert.notNull(groupByField, "Group by field must not be null!");
+
+ this.groupByField = groupByField;
+ this.groupByExpression = null;
+ this.outputs = Outputs.EMPTY;
+
+ }
+
+ /**
+ * Creates a new {@link BucketOperationSupport} given a {@link AggregationExpression group-by expression}.
+ *
+ * @param groupByExpression must not be {@literal null}.
+ */
+ protected BucketOperationSupport(AggregationExpression groupByExpression) {
+
+ Assert.notNull(groupByExpression, "Group by AggregationExpression must not be null!");
+
+ this.groupByExpression = groupByExpression;
+ this.groupByField = null;
+ this.outputs = Outputs.EMPTY;
+ }
+
+ /**
+ * Creates a copy of {@link BucketOperationSupport}.
+ *
+ * @param operationSupport must not be {@literal null}.
+ */
+ protected BucketOperationSupport(BucketOperationSupport, ?> operationSupport) {
+ this(operationSupport, operationSupport.outputs);
+ }
+
+ /**
+ * Creates a copy of {@link BucketOperationSupport} and applies the new {@link Outputs}.
+ *
+ * @param operationSupport must not be {@literal null}.
+ * @param outputs must not be {@literal null}.
+ */
+ protected BucketOperationSupport(BucketOperationSupport, ?> operationSupport, Outputs outputs) {
+
+ Assert.notNull(operationSupport, "BucketOperationSupport must not be null!");
+ Assert.notNull(outputs, "Outputs must not be null!");
+
+ this.groupByField = operationSupport.groupByField;
+ this.groupByExpression = operationSupport.groupByExpression;
+ this.outputs = outputs;
+ }
+
+ /**
+ * Creates a new {@link ExpressionBucketOperationBuilderSupport} given a SpEL {@literal expression} and optional
+ * {@literal params} to add an output field to the resulting bucket documents.
+ *
+ * @param expression the SpEL expression, must not be {@literal null} or empty.
+ * @param params must not be {@literal null}
+ * @return
+ */
+ public abstract ExpressionBucketOperationBuilderSupport andOutputExpression(String expression,
+ Object... params);
+
+ /**
+ * Creates a new {@link BucketOperationSupport} given an {@link AggregationExpression} to add an output field to the
+ * resulting bucket documents.
+ *
+ * @param expression the SpEL expression, must not be {@literal null} or empty.
+ * @return
+ */
+ public abstract B andOutput(AggregationExpression expression);
+
+ /**
+ * Creates a new {@link BucketOperationSupport} given {@literal fieldName} to add an output field to the resulting
+ * bucket documents. {@link BucketOperationSupport} exposes accumulation operations that can be applied to
+ * {@literal fieldName}.
+ *
+ * @param fieldName must not be {@literal null} or empty.
+ * @return
+ */
+ public abstract B andOutput(String fieldName);
+
+ /**
+ * Creates a new {@link BucketOperationSupport} given to add a count field to the resulting bucket documents.
+ *
+ * @return
+ */
+ public B andOutputCount() {
+ return andOutput(new AggregationExpression() {
+ @Override
+ public DBObject toDbObject(AggregationOperationContext context) {
+ return new BasicDBObject("$sum", 1);
+ }
+ });
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
+ */
+ @Override
+ public DBObject toDBObject(AggregationOperationContext context) {
+
+ DBObject dbObject = new BasicDBObject();
+
+ dbObject.put("groupBy", groupByExpression == null ? context.getReference(groupByField).toString()
+ : groupByExpression.toDbObject(context));
+
+ if (!outputs.isEmpty()) {
+ dbObject.put("output", outputs.toDbObject(context));
+ }
+
+ return dbObject;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields()
+ */
+ @Override
+ public ExposedFields getFields() {
+ return outputs.asExposedFields();
+ }
+
+ /**
+ * Implementation hook to create a new bucket operation.
+ *
+ * @param outputs the outputs
+ * @return the new bucket operation.
+ */
+ protected abstract T newBucketOperation(Outputs outputs);
+
+ protected T andOutput(Output output) {
+ return newBucketOperation(outputs.and(output));
+ }
+
+ /**
+ * Builder for SpEL expression-based {@link Output}.
+ *
+ * @author Mark Paluch
+ */
+ public abstract static class ExpressionBucketOperationBuilderSupport, T extends BucketOperationSupport>
+ extends OutputBuilder {
+
+ /**
+ * Creates a new {@link ExpressionBucketOperationBuilderSupport} for the given value, {@link BucketOperationSupport}
+ * and parameters.
+ *
+ * @param expression must not be {@literal null}.
+ * @param operation must not be {@literal null}.
+ * @param parameters
+ */
+ protected ExpressionBucketOperationBuilderSupport(String expression, T operation, Object[] parameters) {
+ super(new SpelExpressionOutput(expression, parameters), operation);
+ }
+ }
+
+ /**
+ * Base class for {@link Output} builders that result in a {@link BucketOperationSupport} providing the built
+ * {@link Output}.
+ *
+ * @author Mark Paluch
+ */
+ public abstract static class OutputBuilder, T extends BucketOperationSupport> {
+
+ protected final Object value;
+ protected final T operation;
+
+ /**
+ * Creates a new {@link OutputBuilder} for the given value and {@link BucketOperationSupport}.
+ *
+ * @param value must not be {@literal null}.
+ * @param operation must not be {@literal null}.
+ */
+ public OutputBuilder(Object value, T operation) {
+
+ Assert.notNull(value, "Value must not be null or empty!");
+ Assert.notNull(operation, "ProjectionOperation must not be null!");
+
+ this.value = value;
+ this.operation = operation;
+ }
+
+ /**
+ * Generates a builder for a {@code $sum}-expression.
+ *
+ * Count expressions are emulated via {@code $sum: 1}.
+ *
+ *
+ * @return
+ */
+ public B count() {
+ return sum(1);
+ }
+
+ /**
+ * Generates a builder for a {@code $sum}-expression for the current value.
+ *
+ * @return
+ */
+ public B sum() {
+ return apply(Accumulators.SUM);
+ }
+
+ /**
+ * Generates a builder for a {@code $sum}-expression for the given {@literal value}.
+ *
+ * @param value
+ * @return
+ */
+ public B sum(Number value) {
+ return apply(new OperationOutput(Accumulators.SUM.toString(), Collections.singleton(value)));
+ }
+
+ /**
+ * Generates a builder for an {@code $last}-expression for the current value..
+ *
+ * @return
+ */
+ public B last() {
+ return apply(Accumulators.LAST);
+ }
+
+ /**
+ * Generates a builder for a {@code $first}-expression the current value.
+ *
+ * @return
+ */
+ public B first() {
+ return apply(Accumulators.FIRST);
+ }
+
+ /**
+ * Generates a builder for an {@code $avg}-expression for the current value.
+ *
+ * @param reference
+ * @return
+ */
+ public B avg() {
+ return apply(Accumulators.AVG);
+ }
+
+ /**
+ * Generates a builder for an {@code $min}-expression for the current value.
+ *
+ * @return
+ */
+ public B min() {
+ return apply(Accumulators.MIN);
+ }
+
+ /**
+ * Generates a builder for an {@code $max}-expression for the current value.
+ *
+ * @return
+ */
+ public B max() {
+ return apply(Accumulators.MAX);
+ }
+
+ /**
+ * Generates a builder for an {@code $push}-expression for the current value.
+ *
+ * @return
+ */
+ public B push() {
+ return apply(Accumulators.PUSH);
+ }
+
+ /**
+ * Generates a builder for an {@code $addToSet}-expression for the current value.
+ *
+ * @return
+ */
+ public B addToSet() {
+ return apply(Accumulators.ADDTOSET);
+ }
+
+ /**
+ * Apply an operator to the current value.
+ *
+ * @param operation the operation name, must not be {@literal null} or empty.
+ * @param values must not be {@literal null}.
+ * @return
+ */
+ public B apply(String operation, Object... values) {
+
+ Assert.hasText(operation, "Operation must not be empty or null!");
+ Assert.notNull(value, "Values must not be null!");
+
+ List objects = new ArrayList(values.length + 1);
+ objects.add(value);
+ objects.addAll(Arrays.asList(values));
+ return apply(new OperationOutput(operation, objects));
+ }
+
+ /**
+ * Apply an {@link OperationOutput} to this output.
+ *
+ * @param operationOutput must not be {@literal null}.
+ * @return
+ */
+ protected abstract B apply(OperationOutput operationOutput);
+
+ private B apply(Accumulators operation) {
+ return this.apply(operation.toString());
+ }
+
+ /**
+ * Returns the finally to be applied {@link BucketOperation} with the given alias.
+ *
+ * @param alias will never be {@literal null} or empty.
+ * @return
+ */
+ public T as(String alias) {
+
+ if (value instanceof OperationOutput) {
+ return this.operation.andOutput(((OperationOutput) this.value).withAlias(alias));
+ }
+
+ if (value instanceof Field) {
+ throw new IllegalStateException("Cannot add a field as top-level output. Use accumulator expressions.");
+ }
+
+ return this.operation
+ .andOutput(new AggregationExpressionOutput(Fields.field(alias), (AggregationExpression) value));
+ }
+ }
+
+ private enum Accumulators {
+
+ SUM("$sum"), AVG("$avg"), FIRST("$first"), LAST("$last"), MAX("$max"), MIN("$min"), PUSH("$push"), ADDTOSET(
+ "$addToSet");
+
+ private String mongoOperator;
+
+ Accumulators(String mongoOperator) {
+ this.mongoOperator = mongoOperator;
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Enum#toString()
+ */
+ @Override
+ public String toString() {
+ return mongoOperator;
+ }
+ }
+
+ /**
+ * Encapsulates {@link Output}s.
+ *
+ * @author Mark Paluch
+ */
+ protected static class Outputs implements AggregationExpression {
+
+ protected static final Outputs EMPTY = new Outputs();
+
+ private List outputs;
+
+ /**
+ * Creates a new, empty {@link Outputs}.
+ */
+ private Outputs() {
+ this.outputs = new ArrayList();
+ }
+
+ /**
+ * Creates new {@link Outputs} containing all given {@link Output}s.
+ *
+ * @param current
+ * @param output
+ */
+ private Outputs(Collection current, Output output) {
+
+ this.outputs = new ArrayList(current.size() + 1);
+ this.outputs.addAll(current);
+ this.outputs.add(output);
+ }
+
+ /**
+ * @return the {@link ExposedFields} derived from {@link Output}.
+ */
+ protected ExposedFields asExposedFields() {
+
+ ExposedFields fields = ExposedFields.from();
+
+ for (Output output : outputs) {
+ fields = fields.and(output.getExposedField());
+ }
+
+ return fields;
+ }
+
+ /**
+ * Create a new {@link Outputs} that contains the new {@link Output}.
+ *
+ * @param output must not be {@literal null}.
+ * @return the new {@link Outputs} that contains the new {@link Output}
+ */
+ protected Outputs and(Output output) {
+
+ Assert.notNull(output, "BucketOutput must not be null!");
+ return new Outputs(this.outputs, output);
+ }
+
+ /**
+ * @return {@literal true} if {@link Outputs} contains no {@link Output}.
+ */
+ protected boolean isEmpty() {
+ return outputs.isEmpty();
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
+ */
+ @Override
+ public DBObject toDbObject(AggregationOperationContext context) {
+
+ DBObject dbObject = new BasicDBObject();
+
+ for (Output output : outputs) {
+ dbObject.put(output.getExposedField().getName(), output.toDbObject(context));
+ }
+
+ return dbObject;
+ }
+
+ }
+
+ /**
+ * Encapsulates an output field in a bucket aggregation stage.
+ *
+ * Output fields can be either top-level fields that define a valid field name or nested output fields using
+ * operators.
+ *
+ * @author Mark Paluch
+ */
+ protected abstract static class Output implements AggregationExpression {
+
+ private final ExposedField field;
+
+ /**
+ * Creates new {@link Projection} for the given {@link Field}.
+ *
+ * @param field must not be {@literal null}.
+ */
+ protected Output(Field field) {
+
+ Assert.notNull(field, "Field must not be null!");
+ this.field = new ExposedField(field, true);
+ }
+
+ /**
+ * Returns the field exposed by the {@link Output}.
+ *
+ * @return will never be {@literal null}.
+ */
+ protected ExposedField getExposedField() {
+ return field;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
+ */
+ @Override
+ public abstract DBObject toDbObject(AggregationOperationContext context);
+ }
+
+ /**
+ * Output field that uses a Mongo operation (expression object) to generate an output field value.
+ *
+ * {@link OperationOutput} is used either with a regular field name or an operation keyword (e.g.
+ * {@literal $sum, $count}).
+ *
+ * @author Mark Paluch
+ */
+ protected static class OperationOutput extends Output {
+
+ private final String operation;
+ private final List values;
+
+ /**
+ * Creates a new {@link Output} for the given field.
+ *
+ * @param operation the actual operation key, must not be {@literal null} or empty.
+ * @param values the values to pass into the operation, must not be {@literal null}.
+ */
+ public OperationOutput(String operation, Collection extends Object> values) {
+
+ super(Fields.field(operation));
+
+ Assert.hasText(operation, "Operation must not be null or empty!");
+ Assert.notNull(values, "Values must not be null!");
+
+ this.operation = operation;
+ this.values = new ArrayList(values);
+ }
+
+ private OperationOutput(Field field, OperationOutput operationOutput) {
+
+ super(field);
+
+ this.operation = operationOutput.operation;
+ this.values = operationOutput.values;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
+ */
+ @Override
+ public DBObject toDbObject(AggregationOperationContext context) {
+
+ List operationArguments = getOperationArguments(context);
+ return new BasicDBObject(operation,
+ operationArguments.size() == 1 ? operationArguments.get(0) : operationArguments);
+ }
+
+ protected List getOperationArguments(AggregationOperationContext context) {
+
+ List result = new ArrayList(values != null ? values.size() : 1);
+
+ for (Object element : values) {
+
+ if (element instanceof Field) {
+ result.add(context.getReference((Field) element).toString());
+ } else if (element instanceof Fields) {
+ for (Field field : (Fields) element) {
+ result.add(context.getReference(field).toString());
+ }
+ } else if (element instanceof AggregationExpression) {
+ result.add(((AggregationExpression) element).toDbObject(context));
+ } else {
+ result.add(element);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the field that holds the {@link ProjectionOperationBuilder.OperationProjection}.
+ *
+ * @return
+ */
+ protected Field getField() {
+ return getExposedField();
+ }
+
+ /**
+ * Creates a new instance of this {@link OperationOutput} with the given alias.
+ *
+ * @param alias the alias to set
+ * @return
+ */
+ public OperationOutput withAlias(String alias) {
+
+ final Field aliasedField = Fields.field(alias);
+ return new OperationOutput(aliasedField, this) {
+
+ @Override
+ protected Field getField() {
+ return aliasedField;
+ }
+
+ @Override
+ protected List getOperationArguments(AggregationOperationContext context) {
+
+ // We have to make sure that we use the arguments from the "previous" OperationOutput that we replace
+ // with this new instance.
+
+ return OperationOutput.this.getOperationArguments(context);
+ }
+ };
+ }
+ }
+
+ /**
+ * A {@link Output} based on a SpEL expression.
+ */
+ static class SpelExpressionOutput extends Output {
+
+ private static final SpelExpressionTransformer TRANSFORMER = new SpelExpressionTransformer();
+
+ private final String expression;
+ private final Object[] params;
+
+ /**
+ * Creates a new {@link SpelExpressionOutput} for the given field, SpEL expression and parameters.
+ *
+ * @param expression must not be {@literal null} or empty.
+ * @param parameters must not be {@literal null}.
+ */
+ public SpelExpressionOutput(String expression, Object[] parameters) {
+
+ super(Fields.field(expression));
+
+ Assert.hasText(expression, "Expression must not be null!");
+ Assert.notNull(parameters, "Parameters must not be null!");
+
+ this.expression = expression;
+ this.params = parameters.clone();
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.Output#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
+ */
+ @Override
+ public DBObject toDbObject(AggregationOperationContext context) {
+ return (DBObject) toMongoExpression(context, expression, params);
+ }
+
+ protected static Object toMongoExpression(AggregationOperationContext context, String expression, Object[] params) {
+ return TRANSFORMER.transform(expression, context, params);
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ */
+ private static class AggregationExpressionOutput extends Output {
+
+ private final AggregationExpression expression;
+
+ /**
+ * Creates a new {@link AggregationExpressionOutput}.
+ *
+ * @param field
+ * @param expression
+ */
+ protected AggregationExpressionOutput(Field field, AggregationExpression expression) {
+
+ super(field);
+
+ this.expression = expression;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.Output#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
+ */
+ @Override
+ public DBObject toDbObject(AggregationOperationContext context) {
+ return expression.toDbObject(context);
+ }
+ }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java
index 2d0bb7750..7a7981fd9 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java
@@ -148,6 +148,7 @@ public class AggregationTests {
mongoTemplate.dropCollection(Sales.class);
mongoTemplate.dropCollection(Sales2.class);
mongoTemplate.dropCollection(Employee.class);
+ mongoTemplate.dropCollection(Art.class);
}
/**
@@ -1667,6 +1668,46 @@ public class AggregationTests {
assertThat((DBObject) list.get(1), isBsonObject().containing("name", "Eliot").containing("depth", 0L));
}
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void bucketShouldCollectDocumentsIntoABucket() {
+
+ assumeTrue(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_FOUR));
+
+ Art a1 = Art.builder().id(1).title("The Pillars of Society").artist("Grosz").year(1926).price(199.99).build();
+ Art a2 = Art.builder().id(2).title("Melancholy III").artist("Munch").year(1902).price(280.00).build();
+ Art a3 = Art.builder().id(3).title("Dancer").artist("Miro").year(1925).price(76.04).build();
+ Art a4 = Art.builder().id(4).title("The Great Wave off Kanagawa").artist("Hokusai").price(167.30).build();
+
+ mongoTemplate.insert(Arrays.asList(a1, a2, a3, a4), Art.class);
+
+ TypedAggregation aggregation = newAggregation(Art.class, //
+ bucket("price") //
+ .withBoundaries(0, 100, 200) //
+ .withDefaultBucket("other") //
+ .andOutputCount().as("count") //
+ .andOutput("title").push().as("titles") //
+ .andOutputExpression("price * 10").sum().as("sum"));
+
+ AggregationResults result = mongoTemplate.aggregate(aggregation, DBObject.class);
+ assertThat(result.getMappedResults().size(), is(3));
+
+ // { "_id" : 0 , "count" : 1 , "titles" : [ "Dancer"] , "sum" : 760.4000000000001}
+ DBObject bound0 = result.getMappedResults().get(0);
+ assertThat(bound0, isBsonObject().containing("count", 1).containing("titles.[0]", "Dancer"));
+ assertThat((Double) bound0.get("sum"), is(closeTo(760.40, 0.1)));
+
+ // { "_id" : 100 , "count" : 2 , "titles" : [ "The Pillars of Society" , "The Great Wave off Kanagawa"] , "sum" :
+ // 3672.9}
+ DBObject bound100 = result.getMappedResults().get(1);
+ assertThat(bound100, isBsonObject().containing("count", 2).containing("_id", 100));
+ assertThat((List) bound100.get("titles"),
+ hasItems("The Pillars of Society", "The Great Wave off Kanagawa"));
+ assertThat((Double) bound100.get("sum"), is(closeTo(3672.9, 0.1)));
+ }
+
private void createUsersWithReferencedPersons() {
mongoTemplate.dropCollection(User.class);
@@ -1956,4 +1997,18 @@ public class AggregationTests {
String name;
String reportsTo;
}
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @lombok.Data
+ @Builder
+ static class Art {
+
+ int id;
+ String title;
+ String artist;
+ Integer year;
+ double price;
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/BucketOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/BucketOperationUnitTests.java
new file mode 100644
index 000000000..f2ff2ba12
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/BucketOperationUnitTests.java
@@ -0,0 +1,254 @@
+/*
+ * 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 static org.hamcrest.core.Is.*;
+import static org.junit.Assert.*;
+import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
+
+import org.junit.Test;
+import org.springframework.data.mongodb.core.aggregation.AggregationExpressions.ArithmeticOperators;
+
+import com.mongodb.DBObject;
+import com.mongodb.util.JSON;
+
+/**
+ * Unit tests for {@link BucketOperation}.
+ *
+ * @author Mark Paluch
+ */
+public class BucketOperationUnitTests {
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void rejectsNullFields() {
+ new BucketOperation((Field) null);
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderBucketOutputExpressions() {
+
+ BucketOperation operation = Aggregation.bucket("field") //
+ .andOutputExpression("(netPrice + surCharge) * taxrate * [0]", 2).as("grossSalesPrice") //
+ .andOutput("title").push().as("titles");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject), is(JSON.parse(
+ "{ \"grossSalesPrice\" : { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\" , 2]} , \"titles\" : { $push: \"$title\" } }}")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test(expected = IllegalStateException.class)
+ public void shouldRenderEmptyAggregationExpression() {
+ bucket("groupby").andOutput("field").as("alias");
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderBucketOutputOperators() {
+
+ BucketOperation operation = Aggregation.bucket("field") //
+ .andOutputCount().as("titles");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject), is(JSON.parse("{ titles : { $sum: 1 } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderSumAggregationExpression() {
+
+ DBObject agg = bucket("field") //
+ .andOutput(ArithmeticOperators.valueOf("quizzes").sum()).as("quizTotal") //
+ .toDBObject(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(agg, is(JSON.parse(
+ "{ $bucket: { groupBy: \"$field\", boundaries: [], output : { quizTotal: { $sum: \"$quizzes\"} } } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderDefault() {
+
+ DBObject agg = bucket("field").withDefaultBucket("default bucket").toDBObject(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(agg,
+ is(JSON.parse("{ $bucket: { groupBy: \"$field\", boundaries: [], default: \"default bucket\" } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderBoundaries() {
+
+ DBObject agg = bucket("field") //
+ .withDefaultBucket("default bucket") //
+ .withBoundaries(0) //
+ .withBoundaries(10, 20).toDBObject(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(agg,
+ is(JSON.parse("{ $bucket: { boundaries: [0, 10, 20], default: \"default bucket\", groupBy: \"$field\" } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderSumOperator() {
+
+ BucketOperation operation = bucket("field") //
+ .andOutput("score").sum().as("cummulated_score");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject), is(JSON.parse("{ cummulated_score : { $sum: \"$score\" } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderSumWithValueOperator() {
+
+ BucketOperation operation = bucket("field") //
+ .andOutput("score").sum(4).as("cummulated_score");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject), is(JSON.parse("{ cummulated_score : { $sum: 4 } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderAvgOperator() {
+
+ BucketOperation operation = bucket("field") //
+ .andOutput("score").avg().as("average");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject), is(JSON.parse("{ average : { $avg: \"$score\" } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderFirstOperator() {
+
+ BucketOperation operation = bucket("field") //
+ .andOutput("title").first().as("first_title");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject), is(JSON.parse("{ first_title : { $first: \"$title\" } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderLastOperator() {
+
+ BucketOperation operation = bucket("field") //
+ .andOutput("title").last().as("last_title");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject), is(JSON.parse("{ last_title : { $last: \"$title\" } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderMinOperator() {
+
+ BucketOperation operation = bucket("field") //
+ .andOutput("score").min().as("min_score");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject), is(JSON.parse("{ min_score : { $min: \"$score\" } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderPushOperator() {
+
+ BucketOperation operation = bucket("field") //
+ .andOutput("title").push().as("titles");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject), is(JSON.parse("{ titles : { $push: \"$title\" } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderAddToSetOperator() {
+
+ BucketOperation operation = bucket("field") //
+ .andOutput("title").addToSet().as("titles");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject), is(JSON.parse("{ titles : { $addToSet: \"$title\" } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderSumWithExpression() {
+
+ BucketOperation operation = bucket("field") //
+ .andOutputExpression("netPrice + tax").sum().as("total");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject), is(JSON.parse("{ total : { $sum: { $add : [\"$netPrice\", \"$tax\"]} } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderSumWithOwnOutputExpression() {
+
+ BucketOperation operation = bucket("field") //
+ .andOutputExpression("netPrice + tax").apply("$multiply", 5).as("total");
+
+ DBObject dbObject = operation.toDBObject(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(dbObject),
+ is(JSON.parse("{ total : { $multiply: [ {$add : [\"$netPrice\", \"$tax\"]}, 5] } }")));
+ }
+
+ private static DBObject extractOutput(DBObject fromBucketClause) {
+ return (DBObject) ((DBObject) fromBucketClause.get("$bucket")).get("output");
+ }
+}