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