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 d03fd9da5..edc555003 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
@@ -417,7 +417,7 @@ public class Aggregation {
}
/**
- * Creates a new {@link BucketOperation} using given {@literal groupByField}.
+ * Creates a new {@link BucketOperation} given {@literal groupByField}.
*
* @param groupByField must not be {@literal null} or empty.
* @return
@@ -427,7 +427,7 @@ public class Aggregation {
}
/**
- * Creates a new {@link BucketOperation} using given {@link AggregationExpression group-by expression}.
+ * Creates a new {@link BucketOperation} given {@link AggregationExpression group-by expression}.
*
* @param groupByExpression must not be {@literal null}.
* @return
@@ -436,6 +436,28 @@ public class Aggregation {
return new BucketOperation(groupByExpression);
}
+ /**
+ * Creates a new {@link BucketAutoOperation} given {@literal groupByField}.
+ *
+ * @param groupByField must not be {@literal null} or empty.
+ * @param buckets number of buckets, must be a positive integer.
+ * @return
+ */
+ public static BucketAutoOperation bucketAuto(String groupByField, int buckets) {
+ return new BucketAutoOperation(field(groupByField), buckets);
+ }
+
+ /**
+ * Creates a new {@link BucketAutoOperation} given {@link AggregationExpression group-by expression}.
+ *
+ * @param groupByExpression must not be {@literal null}.
+ * @param buckets number of buckets, must be a positive integer.
+ * @return
+ */
+ public static BucketAutoOperation bucketAuto(AggregationExpression groupByExpression, int buckets) {
+ return new BucketAutoOperation(groupByExpression, buckets);
+ }
+
/**
* Creates a new {@link LookupOperation}.
*
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java
new file mode 100644
index 000000000..24c8ced54
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java
@@ -0,0 +1,272 @@
+/*
+ * 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 org.springframework.data.mongodb.core.aggregation.BucketAutoOperation.BucketAutoOperationOutputBuilder;
+import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder;
+import org.springframework.util.Assert;
+
+import org.bson.Document;
+
+/**
+ * Encapsulates the aggregation framework {@code $bucketAuto}-operation.
+ *
+ * Bucket stage is typically used with {@link Aggregation} and {@code $facet}. Categorizes incoming documents into a
+ * specific number of groups, called buckets, based on a specified expression. Bucket boundaries are automatically
+ * determined in an attempt to evenly distribute the documents into the specified number of buckets.
+ *
+ * We recommend to use the static factory method {@link Aggregation#bucketAuto(String, int)} instead of creating instances of
+ * this class directly.
+ *
+ * @see http://docs.mongodb.org/manual/reference/aggregation/bucketAuto/
+ * @see BucketOperationSupport
+ * @author Mark Paluch
+ * @since 1.10
+ */
+public class BucketAutoOperation extends BucketOperationSupport
+ implements FieldsExposingAggregationOperation {
+
+ private final int buckets;
+ private final String granularity;
+
+ /**
+ * Creates a new {@link BucketAutoOperation} given a {@link Field group-by field}.
+ *
+ * @param groupByField must not be {@literal null}.
+ * @param buckets number of buckets, must be a positive integer.
+ */
+ public BucketAutoOperation(Field groupByField, int buckets) {
+
+ super(groupByField);
+
+ Assert.isTrue(buckets > 0, "Number of buckets must be greater 0!");
+
+ this.buckets = buckets;
+ this.granularity = null;
+ }
+
+ /**
+ * Creates a new {@link BucketAutoOperation} given a {@link AggregationExpression group-by expression}.
+ *
+ * @param groupByExpression must not be {@literal null}.
+ * @param buckets number of buckets, must be a positive integer.
+ */
+ public BucketAutoOperation(AggregationExpression groupByExpression, int buckets) {
+
+ super(groupByExpression);
+
+ Assert.isTrue(buckets > 0, "Number of buckets must be greater 0!");
+
+ this.buckets = buckets;
+ this.granularity = null;
+ }
+
+ private BucketAutoOperation(BucketAutoOperation bucketOperation, Outputs outputs) {
+
+ super(bucketOperation, outputs);
+
+ this.buckets = bucketOperation.buckets;
+ this.granularity = bucketOperation.granularity;
+ }
+
+ private BucketAutoOperation(BucketAutoOperation bucketOperation, int buckets, String granularity) {
+
+ super(bucketOperation);
+
+ this.buckets = buckets;
+ this.granularity = granularity;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
+ */
+ @Override
+ public Document toDocument(AggregationOperationContext context) {
+
+ Document options = new Document();
+
+ options.put("buckets", buckets);
+
+ if (granularity != null) {
+ options.put("granularity", granularity);
+ }
+
+ options.putAll(super.toDocument(context));
+
+ return new Document("$bucketAuto", options);
+ }
+
+ /**
+ * Configures a number of bucket {@literal buckets} and return a new {@link BucketAutoOperation}.
+ *
+ * @param buckets must be a positive number.
+ * @return
+ */
+ public BucketAutoOperation withBuckets(int buckets) {
+
+ Assert.isTrue(buckets > 0, "Number of buckets must be greater 0!");
+ return new BucketAutoOperation(this, buckets, granularity);
+ }
+
+ /**
+ * Configures {@literal granularity} that specifies the preferred number series to use to ensure that the calculated
+ * boundary edges end on preferred round numbers or their powers of 10 and return a new {@link BucketAutoOperation}.
+ *
+ * @param granularity must not be {@literal null}.
+ * @return
+ */
+ public BucketAutoOperation withGranularity(Granularity granularity) {
+
+ Assert.notNull(granularity, "Granularity must not be null!");
+
+ return new BucketAutoOperation(this, buckets, granularity.toMongoGranularity());
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#newBucketOperation(org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.Outputs)
+ */
+ @Override
+ protected BucketAutoOperation newBucketOperation(Outputs outputs) {
+ return new BucketAutoOperation(this, outputs);
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#andOutputExpression(java.lang.String, java.lang.Object[])
+ */
+ @Override
+ public ExpressionBucketAutoOperationBuilder andOutputExpression(String expression, Object... params) {
+ return new ExpressionBucketAutoOperationBuilder(expression, this, params);
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#andOutput(org.springframework.data.mongodb.core.aggregation.AggregationExpression)
+ */
+ @Override
+ public BucketAutoOperationOutputBuilder andOutput(AggregationExpression expression) {
+ return new BucketAutoOperationOutputBuilder(expression, this);
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.BucketOperationSupport#andOutput(java.lang.String)
+ */
+ @Override
+ public BucketAutoOperationOutputBuilder andOutput(String fieldName) {
+ return new BucketAutoOperationOutputBuilder(Fields.field(fieldName), this);
+ }
+
+ /**
+ * {@link OutputBuilder} implementation for {@link BucketAutoOperation}.
+ */
+ public static class BucketAutoOperationOutputBuilder
+ extends OutputBuilder {
+
+ /**
+ * Creates a new {@link BucketAutoOperationOutputBuilder} fot the given value and {@link BucketAutoOperation}.
+ *
+ * @param value must not be {@literal null}.
+ * @param operation must not be {@literal null}.
+ */
+ protected BucketAutoOperationOutputBuilder(Object value, BucketAutoOperation 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 BucketAutoOperationOutputBuilder apply(OperationOutput operationOutput) {
+ return new BucketAutoOperationOutputBuilder(operationOutput, this.operation);
+ }
+ }
+
+ /**
+ * {@link ExpressionBucketOperationBuilderSupport} implementation for {@link BucketAutoOperation} using SpEL
+ * expression based {@link Output}.
+ *
+ * @author Mark Paluch
+ */
+ public static class ExpressionBucketAutoOperationBuilder
+ extends ExpressionBucketOperationBuilderSupport {
+
+ /**
+ * Creates a new {@link ExpressionBucketAutoOperationBuilder} for the given value, {@link BucketAutoOperation} and
+ * parameters.
+ *
+ * @param expression must not be {@literal null}.
+ * @param operation must not be {@literal null}.
+ * @param parameters
+ */
+ protected ExpressionBucketAutoOperationBuilder(String expression, BucketAutoOperation 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 BucketAutoOperationOutputBuilder apply(OperationOutput operationOutput) {
+ return new BucketAutoOperationOutputBuilder(operationOutput, this.operation);
+ }
+ }
+
+ /**
+ * @author Mark Paluch
+ */
+ public static interface Granularity {
+
+ /**
+ * @return a String that represents a MongoDB granularity to be used with {@link BucketAutoOperation}.
+ */
+ String toMongoGranularity();
+ }
+
+ /**
+ * Supported MongoDB granularities.
+ *
+ * @see https://en.wikipedia.org/wiki/Preferred_number
+ * @see https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/#granularity
+ * @author Mark Paluch
+ */
+ public enum Granularities implements Granularity {
+
+ R5, R10, R20, R40, R80, //
+
+ SERIES_1_2_5("1-2-5"), //
+
+ E6, E12, E24, E48, E96, E192, //
+
+ POWERSOF2;
+
+ final String granularity;
+
+ Granularities() {
+ this.granularity = name();
+ }
+
+ Granularities(String granularity) {
+ this.granularity = granularity;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.aggregation.GranularitytoMongoGranularity()
+ */
+ @Override
+ public String toMongoGranularity() {
+ return granularity;
+ }
+ }
+}
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
index eaa41207c..bac59bba2 100644
--- 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
@@ -221,7 +221,7 @@ public abstract class BucketOperationSupport aggregation = newAggregation(Art.class, //
+ bucketAuto(Multiply.valueOf("price").multiplyBy(10), 3) //
+ .withGranularity(Granularities.E12) //
+ .andOutputCount().as("count") //
+ .andOutput("title").push().as("titles") //
+ .andOutputExpression("price * 10").sum().as("sum"));
+
+ AggregationResults result = mongoTemplate.aggregate(aggregation, Document.class);
+ assertThat(result.getMappedResults().size(), is(3));
+
+ // { "min" : 680.0 , "max" : 820.0 , "count" : 1 , "titles" : [ "Dancer"] , "sum" : 760.4000000000001}
+ Document bound0 = result.getMappedResults().get(0);
+ assertThat(bound0, isBsonObject().containing("count", 1).containing("titles.[0]", "Dancer").containing("min", 680.0)
+ .containing("max"));
+
+ // { "min" : 820.0 , "max" : 1800.0 , "count" : 1 , "titles" : [ "The Great Wave off Kanagawa"] , "sum" : 1673.0}
+ Document bound1 = result.getMappedResults().get(1);
+ assertThat(bound1, isBsonObject().containing("count", 1).containing("min", 820.0));
+ assertThat((List) bound1.get("titles"), hasItems("The Great Wave off Kanagawa"));
+ assertThat((Double) bound1.get("sum"), is(closeTo(1673.0, 0.1)));
+ }
+
private void createUsersWithReferencedPersons() {
mongoTemplate.dropCollection(User.class);
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperationUnitTests.java
new file mode 100644
index 000000000..41e303e5c
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperationUnitTests.java
@@ -0,0 +1,141 @@
+/*
+ * 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.DocumentTestUtils.getAsDocument;
+import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
+
+import org.junit.Test;
+import org.springframework.data.mongodb.core.aggregation.BucketAutoOperation.Granularities;
+
+import org.bson.Document;
+
+/**
+ * Unit tests for {@link BucketAutoOperation}.
+ *
+ * @author Mark Paluch
+ */
+public class BucketAutoOperationUnitTests {
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void rejectsNullFields() {
+ new BucketAutoOperation((Field) null, 0);
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void rejectsNonPositiveIntegerNullFields() {
+ new BucketAutoOperation(Fields.field("field"), 0);
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderBucketOutputExpressions() {
+
+ BucketAutoOperation operation = Aggregation.bucketAuto("field", 5) //
+ .andOutputExpression("(netPrice + surCharge) * taxrate * [0]", 2).as("grossSalesPrice") //
+ .andOutput("title").push().as("titles");
+
+ Document agg = operation.toDocument(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(agg), is(Document.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() {
+
+ BucketAutoOperation operation = Aggregation.bucketAuto("field", 5) //
+ .andOutputCount().as("titles");
+
+ Document agg = operation.toDocument(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(agg), is(Document.parse("{ titles : { $sum: 1 } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderCorrectly() {
+
+ Document agg = bucketAuto("field", 1).withBuckets(5).toDocument(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(agg, is(Document.parse("{ $bucketAuto: { groupBy: \"$field\", buckets: 5 } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderGranulariy() {
+
+ Document agg = bucketAuto("field", 1) //
+ .withGranularity(Granularities.E24) //
+ .toDocument(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(agg, is(Document.parse("{ $bucketAuto: { buckets: 1, granularity: \"E24\", groupBy: \"$field\" } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderSumOperator() {
+
+ BucketAutoOperation operation = bucketAuto("field", 5) //
+ .andOutput("score").sum().as("cummulated_score");
+
+ Document agg = operation.toDocument(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(agg), is(Document.parse("{ cummulated_score : { $sum: \"$score\" } }")));
+ }
+
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldRenderSumWithOwnOutputExpression() {
+
+ BucketAutoOperation operation = bucketAuto("field", 5) //
+ .andOutputExpression("netPrice + tax").apply("$multiply", 5).as("total");
+
+ Document agg = operation.toDocument(Aggregation.DEFAULT_CONTEXT);
+ assertThat(extractOutput(agg),
+ is(Document.parse("{ total : { $multiply: [ {$add : [\"$netPrice\", \"$tax\"]}, 5] } }")));
+ }
+
+ private static Document extractOutput(Document fromBucketClause) {
+ return getAsDocument(getAsDocument(fromBucketClause, "$bucketAuto"), "output");
+ }
+}
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
index a1fdc2c34..ac2b442a1 100644
--- 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
@@ -15,7 +15,8 @@
*/
package org.springframework.data.mongodb.core.aggregation;
-import static org.hamcrest.core.Is.*;
+import static org.hamcrest.Matchers.*;
+import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
@@ -247,6 +248,18 @@ public class BucketOperationUnitTests {
is(Document.parse("{ total : { $multiply: [ {$add : [\"$netPrice\", \"$tax\"]}, 5] } }")));
}
+ /**
+ * @see DATAMONGO-1552
+ */
+ @Test
+ public void shouldExposeDefaultCountField() {
+
+ BucketOperation operation = bucket("field");
+
+ assertThat(operation.getFields().exposesSingleFieldOnly(), is(true));
+ assertThat(operation.getFields().getField("count"), is(notNullValue()));
+ }
+
private static Document extractOutput(Document fromBucketClause) {
return (Document) ((Document) fromBucketClause.get("$bucket")).get("output");
}