From 1080d0c93b01220d8be926947dd6e50582732a25 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 24 Oct 2016 19:13:05 +0200 Subject: [PATCH 1/2] DATAMONGO-1491 - Add support for $filter to aggregation. Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-cross-store/pom.xml | 4 ++-- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb-log4j/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index ea80a3cb74..a0f34d1dcc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1491-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index ae0a5d6c8f..01cfa0221d 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1491-SNAPSHOT ../pom.xml @@ -48,7 +48,7 @@ org.springframework.data spring-data-mongodb - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1491-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 2d02722262..2d65fec229 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1491-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-log4j/pom.xml b/spring-data-mongodb-log4j/pom.xml index ee5e3336db..7a4caed731 100644 --- a/spring-data-mongodb-log4j/pom.xml +++ b/spring-data-mongodb-log4j/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1491-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 8072d3f665..a48186037f 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-1491-SNAPSHOT ../pom.xml From 503fd50b7b7c0faa86d16d051766834fcedb5eb7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 25 Oct 2016 15:43:55 +0200 Subject: [PATCH 2/2] DATAMONGO-1491 - Add support for $filter (aggregation). We new support $filter in aggregation pipeline. Aggregation.newAggregation(Sales.class, Aggregation.project() .and(filter("items").as("item").by(GTE.of(field("item.price"), 100))) .as("items")) --- .../mongodb/core/aggregation/Aggregation.java | 5 +- .../aggregation/AggregationExpressions.java | 293 ++++++++++++++++++ .../AggregationFunctionExpressions.java | 2 +- .../core/aggregation/ExposedFields.java | 124 +++++++- ...osedFieldsAggregationOperationContext.java | 7 +- ...ExpressionAggregationOperationContext.java | 73 +++++ .../core/aggregation/ProjectionOperation.java | 13 + .../TypeBasedAggregationOperationContext.java | 3 +- .../data/mongodb/core/DBObjectTestUtils.java | 6 + .../core/aggregation/AggregationTests.java | 57 ++++ .../FilterExpressionUnitTests.java | 141 +++++++++ ...dAggregationOperationContextUnitTests.java | 3 +- src/main/asciidoc/reference/mongodb.adoc | 2 +- 13 files changed, 706 insertions(+), 23 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FilterExpressionUnitTests.java 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 2b0a45b469..3b5d45cee9 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 @@ -25,6 +25,7 @@ import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; import org.springframework.data.mongodb.core.query.Criteria; @@ -557,7 +558,7 @@ public DBObject getMappedObject(DBObject dbObject) { */ @Override public FieldReference getReference(Field field) { - return new FieldReference(new ExposedField(field, true)); + return new DirectFieldReference(new ExposedField(field, true)); } /* @@ -566,7 +567,7 @@ public FieldReference getReference(Field field) { */ @Override public FieldReference getReference(String name) { - return new FieldReference(new ExposedField(new AggregationField(name), true)); + return new DirectFieldReference(new ExposedField(new AggregationField(name), true)); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java new file mode 100644 index 0000000000..b8a78b31a4 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressions.java @@ -0,0 +1,293 @@ +/* + * 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.List; + +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; +import org.springframework.util.Assert; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * @author Christoph Strobl + * @since 1.10 + */ +public interface AggregationExpressions { + + /** + * {@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 + */ + class Filter implements AggregationExpression { + + private Object input; + private ExposedField as; + private Object condition; + + private Filter() { + // used by builder + } + + /** + * Set the {@literal field} to apply the {@code $filter} to. + * + * @param field must not be {@literal null}. + * @return never {@literal null}. + */ + public static AsBuilder filter(String field) { + + Assert.notNull(field, "Field must not be null!"); + return filter(Fields.field(field)); + } + + /** + * Set the {@literal field} to apply the {@code $filter} to. + * + * @param field must not be {@literal null}. + * @return never {@literal null}. + */ + public static AsBuilder filter(Field field) { + + Assert.notNull(field, "Field must not be null!"); + return new FilterExpressionBuilder().filter(field); + } + + /** + * Set the {@literal values} to apply the {@code $filter} to. + * + * @param values must not be {@literal null}. + * @return + */ + public static AsBuilder filter(List values) { + + Assert.notNull(values, "Values must not be null!"); + return new FilterExpressionBuilder().filter(values); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public DBObject toDbObject(final AggregationOperationContext context) { + + return toFilter(new ExposedFieldsAggregationOperationContext(ExposedFields.from(as), context) { + + @Override + public FieldReference getReference(Field field) { + + FieldReference ref = null; + try { + ref = context.getReference(field); + } catch (Exception e) { + // just ignore that one. + } + return ref != null ? ref : super.getReference(field); + } + }); + } + + private DBObject toFilter(AggregationOperationContext context) { + + DBObject filterExpression = new BasicDBObject(); + + filterExpression.putAll(context.getMappedObject(new BasicDBObject("input", getMappedInput(context)))); + filterExpression.put("as", as.getTarget()); + + filterExpression.putAll(context.getMappedObject(new BasicDBObject("cond", getMappedCondition(context)))); + + return new BasicDBObject("$filter", filterExpression); + } + + private Object getMappedInput(AggregationOperationContext context) { + return input instanceof Field ? context.getReference((Field) input).toString() : input; + } + + private Object getMappedCondition(AggregationOperationContext context) { + + if (!(condition instanceof AggregationExpression)) { + return condition; + } + + NestedDelegatingExpressionAggregationOperationContext nea = new NestedDelegatingExpressionAggregationOperationContext(context); + DBObject mappedCondition = ((AggregationExpression) condition).toDbObject(nea); + return mappedCondition; + } + + /** + * @author Christoph Strobl + */ + public interface InputBuilder { + + /** + * Set the {@literal values} to apply the {@code $filter} to. + * + * @param array must not be {@literal null}. + * @return + */ + AsBuilder filter(List array); + + /** + * Set the {@literal field} holding an array to apply the {@code $filter} to. + * + * @param field must not be {@literal null}. + * @return + */ + AsBuilder filter(Field field); + } + + /** + * @author Christoph Strobl + */ + public interface AsBuilder { + + /** + * Set the {@literal variableName} for the elements in the input array. + * + * @param variableName must not be {@literal null}. + * @return + */ + ConditionBuilder as(String variableName); + } + + /** + * @author Christoph Strobl + */ + public interface ConditionBuilder { + + /** + * Set the {@link AggregationExpression} that determines whether to include the element in the resulting array. + * + * @param expression must not be {@literal null}. + * @return + */ + Filter by(AggregationExpression expression); + + /** + * Set the {@literal expression} that determines whether to include the element in the resulting array. + * + * @param expression must not be {@literal null}. + * @return + */ + Filter by(String expression); + + /** + * Set the {@literal expression} that determines whether to include the element in the resulting array. + * + * @param expression must not be {@literal null}. + * @return + */ + Filter by(DBObject expression); + } + + /** + * @author Christoph Strobl + */ + static final class FilterExpressionBuilder implements InputBuilder, AsBuilder, ConditionBuilder { + + private final Filter filter; + + FilterExpressionBuilder() { + this.filter = new Filter(); + } + + public static InputBuilder newBuilder() { + return new FilterExpressionBuilder(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.InputBuilder#filter(java.util.List) + */ + @Override + public AsBuilder filter(List array) { + + Assert.notNull(array, "Array must not be null!"); + filter.input = new ArrayList(array); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.InputBuilder#filter(org.springframework.data.mongodb.core.aggregation.Field) + */ + @Override + public AsBuilder filter(Field field) { + + Assert.notNull(field, "Field must not be null!"); + filter.input = field; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.AsBuilder#as(java.lang.String) + */ + @Override + public ConditionBuilder as(String variableName) { + + Assert.notNull(variableName, "Variable name must not be null!"); + filter.as = new ExposedField(variableName, true); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.ConditionBuilder#by(org.springframework.data.mongodb.core.aggregation.AggregationExpression) + */ + @Override + public Filter by(AggregationExpression condition) { + + Assert.notNull(condition, "Condition must not be null!"); + filter.condition = condition; + return filter; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.ConditionBuilder#by(java.lang.String) + */ + @Override + public Filter by(String expression) { + + Assert.notNull(expression, "Expression must not be null!"); + filter.condition = expression; + return filter; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpressions.Filter.ConditionBuilder#by(com.mongodb.DBObject) + */ + @Override + public Filter by(DBObject expression) { + + Assert.notNull(expression, "Expression must not be null!"); + filter.condition = expression; + return filter; + } + } + + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationFunctionExpressions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationFunctionExpressions.java index 0b88c039ce..cd669518d8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationFunctionExpressions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationFunctionExpressions.java @@ -33,7 +33,7 @@ */ public enum AggregationFunctionExpressions { - SIZE; + SIZE, GTE; /** * Returns an {@link AggregationExpression} build from the current {@link Enum} name and the given parameters. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java index 02537fcbd7..f9033743db 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java @@ -24,6 +24,7 @@ import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.util.Assert; import org.springframework.util.CompositeIterator; +import org.springframework.util.ObjectUtils; /** * Value object to capture the fields exposed by an {@link AggregationOperation}. @@ -104,7 +105,7 @@ private static ExposedFields createFields(Fields fields, boolean synthetic) { result.add(new ExposedField(field, synthetic)); } - return ExposedFields.from(result); + return from(result); } /** @@ -336,12 +337,36 @@ public int hashCode() { } } + /** + * A reference to an {@link ExposedField}. + * + * @author Christoph Strobl + * @since 1.10 + */ + interface FieldReference { + + /** + * Returns the raw, unqualified reference, i.e. the field reference without a {@literal $} prefix. + * + * @return + */ + String getRaw(); + + /** + * Returns the reference value for the given field reference. Will return 1 for a synthetic, unaliased field or the + * raw rendering of the reference otherwise. + * + * @return + */ + Object getReferenceValue(); + } + /** * A reference to an {@link ExposedField}. * * @author Oliver Gierke */ - static class FieldReference { + static class DirectFieldReference implements FieldReference { private final ExposedField field; @@ -350,17 +375,16 @@ static class FieldReference { * * @param field must not be {@literal null}. */ - public FieldReference(ExposedField field) { + public DirectFieldReference(ExposedField field) { Assert.notNull(field, "ExposedField must not be null!"); this.field = field; } - /** - * Returns the raw, unqualified reference, i.e. the field reference without a {@literal $} prefix. - * - * @return + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference#getRaw() */ public String getRaw() { @@ -368,11 +392,9 @@ public String getRaw() { return field.synthetic ? target : String.format("%s.%s", Fields.UNDERSCORE_ID, target); } - /** - * Returns the reference value for the given field reference. Will return 1 for a synthetic, unaliased field or the - * raw rendering of the reference otherwise. - * - * @return + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference#getReferenceValue() */ public Object getReferenceValue() { return field.synthetic && !field.isAliased() ? 1 : toString(); @@ -398,11 +420,11 @@ public boolean equals(Object obj) { return true; } - if (!(obj instanceof FieldReference)) { + if (!(obj instanceof DirectFieldReference)) { return false; } - FieldReference that = (FieldReference) obj; + DirectFieldReference that = (DirectFieldReference) obj; return this.field.equals(that.field); } @@ -416,4 +438,78 @@ public int hashCode() { return field.hashCode(); } } + + /** + * A {@link FieldReference} to a {@link Field} used within a nested {@link AggregationExpression}. + * + * @author Christoph Strobl + * @since 1.10 + */ + static class ExpressionFieldReference implements FieldReference { + + private FieldReference delegate; + + /** + * Creates a new {@link FieldReference} for the given {@link ExposedField}. + * + * @param field must not be {@literal null}. + */ + public ExpressionFieldReference(FieldReference field) { + delegate = field; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference#getRaw() + */ + @Override + public String getRaw() { + return delegate.getRaw(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference#getReferenceValue() + */ + @Override + public Object getReferenceValue() { + return delegate.getReferenceValue(); + } + + @Override + public String toString() { + + String fieldRef = delegate.toString(); + + if (fieldRef.startsWith("$$")) { + return fieldRef; + } + + if (fieldRef.startsWith("$")) { + return "$" + fieldRef; + } + + return fieldRef; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof ExpressionFieldReference)) { + return false; + } + + ExpressionFieldReference that = (ExpressionFieldReference) obj; + return ObjectUtils.nullSafeEquals(this.delegate, that.delegate); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java index e4c11ae541..9f2f8ba822 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.aggregation; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.util.Assert; @@ -112,10 +113,10 @@ protected FieldReference resolveExposedField(Field field, String name) { if (field != null) { // we return a FieldReference to the given field directly to make sure that we reference the proper alias here. - return new FieldReference(new ExposedField(field, exposedField.isSynthetic())); + return new DirectFieldReference(new ExposedField(field, exposedField.isSynthetic())); } - return new FieldReference(exposedField); + return new DirectFieldReference(exposedField); } if (name.contains(".")) { @@ -126,7 +127,7 @@ protected FieldReference resolveExposedField(Field field, String name) { if (rootField != null) { // We have to synthetic to true, in order to render the field-name as is. - return new FieldReference(new ExposedField(name, true)); + return new DirectFieldReference(new ExposedField(name, true)); } } return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java new file mode 100644 index 0000000000..0e9a71dc9b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java @@ -0,0 +1,73 @@ +/* + * 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.ExposedFields.FieldReference; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExpressionFieldReference; +import org.springframework.util.Assert; + +import com.mongodb.DBObject; + +/** + * {@link AggregationOperationContext} that delegates {@link FieldReference} resolution and mapping to a parent one, but + * assures {@link FieldReference} get converted into {@link ExpressionFieldReference} using {@code $$} to ref an inner + * variable. + * + * @author Christoph Strobl + * @since 1.10 + */ +class NestedDelegatingExpressionAggregationOperationContext implements AggregationOperationContext { + + private final AggregationOperationContext delegate; + + /** + * Creates new {@link NestedDelegatingExpressionAggregationOperationContext}. + * + * @param referenceContext must not be {@literal null}. + */ + public NestedDelegatingExpressionAggregationOperationContext(AggregationOperationContext referenceContext) { + + Assert.notNull(referenceContext, "Reference context must not be null!"); + this.delegate = referenceContext; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getMappedObject(com.mongodb.DBObject) + */ + @Override + public DBObject getMappedObject(DBObject dbObject) { + return delegate.getMappedObject(dbObject); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(org.springframework.data.mongodb.core.aggregation.Field) + */ + @Override + public FieldReference getReference(Field field) { + return new ExpressionFieldReference(delegate.getReference(field)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperationContext#getReference(java.lang.String) + */ + @Override + public FieldReference getReference(String name) { + return new ExpressionFieldReference(delegate.getReference(name)); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java index 73e11bf4dd..f6930c5e4c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java @@ -641,6 +641,19 @@ public ProjectionOperationBuilder slice(int count, int offset) { return project("slice", offset, count); } + /** + * Generates a {@code $filter} expression that returns a subset of the array held by the given field. + * + * @param as The variable name for the element in the input array. Must not be {@literal null}. + * @param condition The {@link AggregationExpression} that determines whether to include the element in the + * resulting array. Must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 + */ + public ProjectionOperationBuilder filter(String as, AggregationExpression condition) { + return this.operation.and(AggregationExpressions.Filter.filter(name).as(as).by(condition)); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java index c800c419c6..d671d97830 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java @@ -22,6 +22,7 @@ import org.springframework.data.mapping.context.PersistentPropertyPath; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -98,6 +99,6 @@ private FieldReference getReferenceFor(Field field) { Field mappedField = field(propertyPath.getLeafProperty().getName(), propertyPath.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE)); - return new FieldReference(new ExposedField(mappedField, true)); + return new DirectFieldReference(new ExposedField(mappedField, true)); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectTestUtils.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectTestUtils.java index f35391e626..f3701bbe16 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectTestUtils.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectTestUtils.java @@ -18,6 +18,8 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import java.util.List; + import com.mongodb.BasicDBList; import com.mongodb.DBObject; @@ -54,6 +56,10 @@ public static BasicDBList getAsDBList(DBObject source, String key) { return getTypedValue(source, key, BasicDBList.class); } + public static List getAsList(DBObject source, String key) { + return getTypedValue(source, key, List.class); + } + /** * Expects the list element with the given index to be a non-{@literal null} {@link DBObject} and returns it. * 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 6a4b8ae81c..0a75c536f0 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 @@ -29,6 +29,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Scanner; @@ -72,6 +73,8 @@ import com.mongodb.MongoException; import com.mongodb.util.JSON; +import lombok.Builder; + /** * Tests for {@link MongoTemplate#aggregate(String, AggregationPipeline, Class)}. * @@ -1500,6 +1503,42 @@ public void sliceShouldBeAppliedCorrectly() { } } + /** + * @see DATAMONGO-1491 + */ + @Test + public void filterShouldBeAppliedCorrectly() { + + assumeTrue(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_TWO)); + + Item item43 = Item.builder().itemId("43").quantity(2).price(2L).build(); + Item item2 = Item.builder().itemId("2").quantity(1).price(240L).build(); + Sales sales1 = Sales.builder().id("0") + .items(Arrays.asList( // + item43, item2)) // + .build(); + + Item item23 = Item.builder().itemId("23").quantity(3).price(110L).build(); + Item item103 = Item.builder().itemId("103").quantity(4).price(5L).build(); + Item item38 = Item.builder().itemId("38").quantity(1).price(300L).build(); + Sales sales2 = Sales.builder().id("1").items(Arrays.asList( // + item23, item103, item38)).build(); + + Item item4 = Item.builder().itemId("4").quantity(1).price(23L).build(); + Sales sales3 = Sales.builder().id("2").items(Arrays.asList( // + item4)).build(); + + mongoTemplate.insert(Arrays.asList(sales1, sales2, sales3), Sales.class); + + TypedAggregation agg = newAggregation(Sales.class, project().and("items") + .filter("item", AggregationFunctionExpressions.GTE.of(field("item.price"), 100)).as("items")); + + assertThat(mongoTemplate.aggregate(agg, Sales.class).getMappedResults(), + contains(Sales.builder().id("0").items(Collections.singletonList(item2)).build(), + Sales.builder().id("1").items(Arrays.asList(item23, item38)).build(), + Sales.builder().id("2").items(Collections. emptyList()).build())); + } + private void createUsersWithReferencedPersons() { mongoTemplate.dropCollection(User.class); @@ -1740,4 +1779,22 @@ public InventoryItem(int id, String item, String description, int qty) { this.qty = qty; } } + + @lombok.Data + @Builder + static class Sales { + + @Id String id; + List items; + } + + @lombok.Data + @Builder + static class Item { + + @org.springframework.data.mongodb.core.mapping.Field("item_id") // + String itemId; + Integer quantity; + Long price; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FilterExpressionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FilterExpressionUnitTests.java new file mode 100644 index 0000000000..5cc4e799f8 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/FilterExpressionUnitTests.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.aggregation.AggregationExpressions.Filter.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.DBObjectTestUtils; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +import com.mongodb.DBObject; +import com.mongodb.util.JSON; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class FilterExpressionUnitTests { + + @Mock MongoDbFactory mongoDbFactory; + + private AggregationOperationContext aggregationContext; + private MongoMappingContext mappingContext; + + @Before + public void setUp() { + + mappingContext = new MongoMappingContext(); + aggregationContext = new TypeBasedAggregationOperationContext(Sales.class, mappingContext, + new QueryMapper(new MappingMongoConverter(new DefaultDbRefResolver(mongoDbFactory), mappingContext))); + } + + /** + * @see DATAMONGO-1491 + */ + @Test + public void shouldConstructFilterExpressionCorrectly() { + + TypedAggregation agg = Aggregation.newAggregation(Sales.class, + Aggregation.project() + .and(filter("items").as("item").by(AggregationFunctionExpressions.GTE.of(Fields.field("item.price"), 100))) + .as("items")); + + DBObject dbo = agg.toDbObject("sales", aggregationContext); + + List pipeline = DBObjectTestUtils.getAsList(dbo, "pipeline"); + DBObject $project = DBObjectTestUtils.getAsDBObject((DBObject) pipeline.get(0), "$project"); + DBObject items = DBObjectTestUtils.getAsDBObject($project, "items"); + DBObject $filter = DBObjectTestUtils.getAsDBObject(items, "$filter"); + + DBObject expected = (DBObject) JSON.parse("{" + // + "input: \"$items\"," + // + "as: \"item\"," + // + "cond: { $gte: [ \"$$item.price\", 100 ] }" + // + "}"); + + assertThat($filter, is(expected)); + } + + /** + * @see DATAMONGO-1491 + */ + @Test + public void shouldConstructFilterExpressionCorrectlyWhenUsingFilterOnProjectionBuilder() { + + TypedAggregation agg = Aggregation.newAggregation(Sales.class, Aggregation.project().and("items") + .filter("item", AggregationFunctionExpressions.GTE.of(Fields.field("item.price"), 100)).as("items")); + + DBObject dbo = agg.toDbObject("sales", aggregationContext); + + List pipeline = DBObjectTestUtils.getAsList(dbo, "pipeline"); + DBObject $project = DBObjectTestUtils.getAsDBObject((DBObject) pipeline.get(0), "$project"); + DBObject items = DBObjectTestUtils.getAsDBObject($project, "items"); + DBObject $filter = DBObjectTestUtils.getAsDBObject(items, "$filter"); + + DBObject expected = (DBObject) JSON.parse("{" + // + "input: \"$items\"," + // + "as: \"item\"," + // + "cond: { $gte: [ \"$$item.price\", 100 ] }" + // + "}"); + + assertThat($filter, is(expected)); + } + + /** + * @see DATAMONGO-1491 + */ + @Test + public void shouldConstructFilterExpressionCorrectlyWhenInputMapToArray() { + + TypedAggregation agg = Aggregation.newAggregation(Sales.class, + Aggregation.project().and(filter(Arrays. asList(1, "a", 2, null, 3.1D, 4, "5")).as("num") + .by(AggregationFunctionExpressions.GTE.of(Fields.field("num"), 3))).as("items")); + + DBObject dbo = agg.toDbObject("sales", aggregationContext); + + List pipeline = DBObjectTestUtils.getAsList(dbo, "pipeline"); + DBObject $project = DBObjectTestUtils.getAsDBObject((DBObject) pipeline.get(0), "$project"); + DBObject items = DBObjectTestUtils.getAsDBObject($project, "items"); + DBObject $filter = DBObjectTestUtils.getAsDBObject(items, "$filter"); + + DBObject expected = (DBObject) JSON.parse("{" + // + "input: [ 1, \"a\", 2, null, 3.1, 4, \"5\" ]," + // + "as: \"num\"," + // + "cond: { $gte: [ \"$$num\", 3 ] }" + // + "}"); + + assertThat($filter, is(expected)); + } + + static class Sales { + + List items; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java index b1371c00c3..c65b98fe15 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java @@ -38,6 +38,7 @@ import org.springframework.data.mapping.model.MappingException; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; +import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; import org.springframework.data.mongodb.core.convert.CustomConversions; import org.springframework.data.mongodb.core.convert.DbRefResolver; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -105,7 +106,7 @@ public void returnsReferencesToNestedFieldsCorrectly() { public void aliasesIdFieldCorrectly() { AggregationOperationContext context = getContext(Foo.class); - assertThat(context.getReference("id"), is(new FieldReference(new ExposedField(field("id", "_id"), true)))); + assertThat(context.getReference("id"), is((FieldReference) new DirectFieldReference(new ExposedField(field("id", "_id"), true)))); } /** diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 6e4c4e7b02..0dad55c133 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1686,7 +1686,7 @@ At the time of this writing we provide support for the following Aggregation Ope | eq (*via: is), gt, gte, lt, lte, ne | Array Aggregation Operators -| size, slice +| size, slice, filter | Conditional Aggregation Operators | cond, ifNull