From 5bd0e21173e7924a44b42db3eeafbfcb1c169cb6 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 23 Nov 2016 10:35:01 +0100 Subject: [PATCH 1/9] DATAMONGO-1527 - Updated changelog. --- src/main/resources/changelog.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt index 6ca345936c..72b5907c92 100644 --- a/src/main/resources/changelog.txt +++ b/src/main/resources/changelog.txt @@ -1,6 +1,18 @@ Spring Data MongoDB Changelog ============================= +Changes in version 2.0.0.M1 (2016-11-23) +---------------------------------------- +* DATAMONGO-1527 - Release 2.0 M1 (Kay). +* DATAMONGO-1509 - Inconsistent type alias placement in list of classes. +* DATAMONGO-1461 - Upgrade Hibernate/JPA dependencies to match Spring 5 baseline. +* DATAMONGO-1448 - Set up 2.0 development. +* DATAMONGO-1444 - Reactive support in Spring Data MongoDB. +* DATAMONGO-1176 - Use org.bson types instead of com.mongodb. +* DATAMONGO-563 - Upgrade to MongoDB driver 2.9.2 as it fixes a serious regression introduced in 2.9.0. +* DATAMONGO-562 - Cannot create entity with OptimisticLocking (@Version) and initial id. + + Changes in version 1.9.5.RELEASE (2016-11-03) --------------------------------------------- * DATAMONGO-1521 - Aggregation.skip(...) expects int but new SkipOperation(...) supports long. From 36838ffe31165bdf69c3bfb8e19ab8a657875278 Mon Sep 17 00:00:00 2001 From: gustavodegeus Date: Sat, 9 Apr 2016 11:46:55 -0400 Subject: [PATCH 2/9] DATAMONGO-1327 - Added support for $stdDevSamp and $stdDevPop to aggregation $group stage. Original Pull Request: #360 CLA: 171720160409030719 (Gustavo de Geus) --- .../core/aggregation/GroupOperation.java | 26 ++++++++++++++-- .../aggregation/GroupOperationUnitTests.java | 31 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java index 4ee8b37ed8..a3d7149707 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -39,6 +39,7 @@ * @author Sebastian Herold * @author Thomas Darimont * @author Oliver Gierke + * @author Gustavo de Geus * @since 1.3 */ public class GroupOperation implements FieldsExposingAggregationOperation { @@ -307,6 +308,27 @@ public GroupOperationBuilder max(AggregationExpression expr) { return newBuilder(GroupOps.MAX, null, expr); } + /** + * Generates an {@link GroupOperationBuilder} for an {@code $stdDevSamp}-expression that for the given + * field-reference. + * + * @param reference + * @return + */ + public GroupOperationBuilder stdDevSamp(String reference) { + return newBuilder(GroupOps.STD_DEV_SAMP, reference, null); + } + + /** + * Generates an {@link GroupOperationBuilder} for an {@code $stdDevPop}-expression that for the given field-reference. + * + * @param reference + * @return + */ + public GroupOperationBuilder stdDevPop(String reference) { + return newBuilder(GroupOps.STD_DEV_POP, reference, null); + } + private GroupOperationBuilder newBuilder(Keyword keyword, String reference, Object value) { return new GroupOperationBuilder(this, new Operation(keyword, null, reference, value)); } @@ -371,7 +393,7 @@ interface Keyword { private static enum GroupOps implements Keyword { - SUM, LAST, FIRST, PUSH, AVG, MIN, MAX, ADD_TO_SET, COUNT; + SUM, LAST, FIRST, PUSH, AVG, MIN, MAX, ADD_TO_SET, COUNT, STD_DEV_SAMP, STD_DEV_POP; @Override public String toString() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java index e2bcb939dd..0bb274e851 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GroupOperationUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -33,6 +33,7 @@ * * @author Oliver Gierke * @author Thomas Darimont + * @author Gustavo de Geus */ public class GroupOperationUnitTests { @@ -204,6 +205,34 @@ public void shouldRenderSizeExpressionInGroup() { assertThat(tagsCount.get("$first"), is((Object) new BasicDBObject("$size", Arrays.asList("$tags")))); } + /** + * @see DATAMONGO-1327 + */ + @Test + public void groupOperationStdDevSampWithValue() { + + GroupOperation groupOperation = Aggregation.group("a", "b").stdDevSamp("field").as("fieldStdDevSamp"); + + DBObject groupClause = extractDbObjectFromGroupOperation(groupOperation); + DBObject push = DBObjectTestUtils.getAsDBObject(groupClause, "fieldStdDevSamp"); + + assertThat(push, is((DBObject) new BasicDBObject("$stdDevSamp", "$field"))); + } + + /** + * @see DATAMONGO-1327 + */ + @Test + public void groupOperationStdDevPopWithValue() { + + GroupOperation groupOperation = Aggregation.group("a", "b").stdDevPop("field").as("fieldStdDevPop"); + + DBObject groupClause = extractDbObjectFromGroupOperation(groupOperation); + DBObject push = DBObjectTestUtils.getAsDBObject(groupClause, "fieldStdDevPop"); + + assertThat(push, is((DBObject) new BasicDBObject("$stdDevPop", "$field"))); + } + private DBObject extractDbObjectFromGroupOperation(GroupOperation groupOperation) { DBObject dbObject = groupOperation.toDBObject(Aggregation.DEFAULT_CONTEXT); DBObject groupClause = DBObjectTestUtils.getAsDBObject(dbObject, "$group"); From 578441ee9ff9a445d7d3e9963303d3376ecd76ca Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 23 Nov 2016 15:09:48 +0100 Subject: [PATCH 3/9] DATAMONGO-1327 - Polishing. Just added overloads for stdDevSamp and stdDevPop taking AggregationExpression and updated the doc. Also replaced String operation based MongoDB operation building by using operators directly. Original Pull Request: #360 --- .../core/aggregation/GroupOperation.java | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java index a3d7149707..092d88c185 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java @@ -40,6 +40,7 @@ * @author Thomas Darimont * @author Oliver Gierke * @author Gustavo de Geus + * @author Christoph Strobl * @since 1.3 */ public class GroupOperation implements FieldsExposingAggregationOperation { @@ -312,23 +313,47 @@ public GroupOperationBuilder max(AggregationExpression expr) { * Generates an {@link GroupOperationBuilder} for an {@code $stdDevSamp}-expression that for the given * field-reference. * - * @param reference - * @return + * @param reference must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 */ public GroupOperationBuilder stdDevSamp(String reference) { return newBuilder(GroupOps.STD_DEV_SAMP, reference, null); } + /** + * Generates an {@link GroupOperationBuilder} for an {@code $stdDevSamp}-expression that for the given {@link AggregationExpression}. + * + * @param expr must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 + */ + public GroupOperationBuilder stdDevSamp(AggregationExpression expr) { + return newBuilder(GroupOps.STD_DEV_SAMP, null, expr); + } + /** * Generates an {@link GroupOperationBuilder} for an {@code $stdDevPop}-expression that for the given field-reference. * - * @param reference - * @return + * @param reference must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 */ public GroupOperationBuilder stdDevPop(String reference) { return newBuilder(GroupOps.STD_DEV_POP, reference, null); } + /** + * Generates an {@link GroupOperationBuilder} for an {@code $stdDevPop}-expression that for the given {@link AggregationExpression}. + * + * @param expr must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 + */ + public GroupOperationBuilder stdDevPop(AggregationExpression expr) { + return newBuilder(GroupOps.STD_DEV_POP, null, expr); + } + private GroupOperationBuilder newBuilder(Keyword keyword, String reference, Object value) { return new GroupOperationBuilder(this, new Operation(keyword, null, reference, value)); } @@ -393,21 +418,18 @@ interface Keyword { private static enum GroupOps implements Keyword { - SUM, LAST, FIRST, PUSH, AVG, MIN, MAX, ADD_TO_SET, COUNT, STD_DEV_SAMP, STD_DEV_POP; - - @Override - public String toString() { + SUM("$sum"), LAST("$last"), FIRST("$first"), PUSH("$push"), AVG("$avg"), MIN("$min"), MAX("$max"), ADD_TO_SET("$addToSet"), STD_DEV_POP("$stdDevPop"), STD_DEV_SAMP("$stdDevSamp"); - String[] parts = name().split("_"); + private String mongoOperator; - StringBuilder builder = new StringBuilder(); + GroupOps(String mongoOperator) { + this.mongoOperator = mongoOperator; + } - for (String part : parts) { - String lowerCase = part.toLowerCase(Locale.US); - builder.append(builder.length() == 0 ? lowerCase : StringUtils.capitalize(lowerCase)); - } - return "$" + builder.toString(); + @Override + public String toString() { + return mongoOperator; } } From 2985b4ca3d5313e48a2d2d80e28e9253e640c4d3 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 25 Oct 2016 15:43:55 +0200 Subject: [PATCH 4/9] 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")) Original pull request: #412. --- .../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 From 3dc1e9355a6119b4e3b6d664cce0ed434816421c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 24 Nov 2016 12:14:04 +0100 Subject: [PATCH 5/9] DATAMONGO-1491 - Polishing. Remove variable before returning value. Add generics for list creation. Original pull request: #412. --- .../mongodb/core/aggregation/AggregationExpressions.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 index b8a78b31a4..e06fa43fb5 100644 --- 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 @@ -130,8 +130,7 @@ private Object getMappedCondition(AggregationOperationContext context) { } NestedDelegatingExpressionAggregationOperationContext nea = new NestedDelegatingExpressionAggregationOperationContext(context); - DBObject mappedCondition = ((AggregationExpression) condition).toDbObject(nea); - return mappedCondition; + return ((AggregationExpression) condition).toDbObject(nea); } /** @@ -223,7 +222,7 @@ public static InputBuilder newBuilder() { public AsBuilder filter(List array) { Assert.notNull(array, "Array must not be null!"); - filter.input = new ArrayList(array); + filter.input = new ArrayList(array); return this; } @@ -287,7 +286,5 @@ public Filter by(DBObject expression) { return filter; } } - } - } From e631e2d7c5fca537207938f984f9845bd0926d54 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 23 Nov 2016 11:38:25 +0100 Subject: [PATCH 6/9] DATAMONGO-784 - Add support for comparison aggregation operators to group & project. We now directly support comparison aggregation operators ($cmp, $eq, $gt, $gte, $lt, $lte and $ne) on both group and project stages. Original pull request: #414. --- .../AggregationFunctionExpressions.java | 2 +- .../core/aggregation/ProjectionOperation.java | 72 ++++++++++++++++ .../ProjectionOperationUnitTests.java | 85 +++++++++++++++++++ 3 files changed, 158 insertions(+), 1 deletion(-) 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 cd669518d8..761f2c3164 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, GTE; + SIZE, CMP, EQ, GT, GTE, LT, LTE, NE; /** * 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/ProjectionOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java index f6930c5e4c..7b74aa8faf 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 @@ -616,6 +616,78 @@ public ProjectionOperationBuilder size() { return project("size"); } + /** + * Generates a {@code $cmp} expression (compare to) that compares the value of the field to a given value or field. + * + * @return never {@literal null}. + * @since 1.10 + */ + public ProjectionOperationBuilder cmp(Object compareValue) { + return project("cmp", compareValue); + } + + /** + * Generates a {@code $eq} expression (equal) that compares the value of the field to a given value or field. + * + * @return never {@literal null}. + * @since 1.10 + */ + public ProjectionOperationBuilder eq(Object compareValue) { + return project("eq", compareValue); + } + + /** + * Generates a {@code $gt} expression (greater than) that compares the value of the field to a given value or field. + * + * @return never {@literal null}. + * @since 1.10 + */ + public ProjectionOperationBuilder gt(Object compareValue) { + return project("gt", compareValue); + } + + /** + * Generates a {@code $gte} expression (greater than equal) that compares the value of the field to a given value or + * field. + * + * @return never {@literal null}. + * @since 1.10 + */ + public ProjectionOperationBuilder gte(Object compareValue) { + return project("gte", compareValue); + } + + /** + * Generates a {@code $lt} expression (less than) that compares the value of the field to a given value or field. + * + * @return never {@literal null}. + * @since 1.10 + */ + public ProjectionOperationBuilder lt(Object compareValue) { + return project("lt", compareValue); + } + + /** + * Generates a {@code $lte} expression (less than equal) that compares the value of the field to a given value or + * field. + * + * @return never {@literal null}. + * @since 1.10 + */ + public ProjectionOperationBuilder lte(Object compareValue) { + return project("lte", compareValue); + } + + /** + * Generates a {@code $ne} expression (not equal) that compares the value of the field to a given value or field. + * + * @return never {@literal null}. + * @since 1.10 + */ + public ProjectionOperationBuilder ne(Object compareValue) { + return project("ne", compareValue); + } + /** * Generates a {@code $slice} expression that returns a subset of the array held by the given field.
* If {@literal n} is positive, $slice returns up to the first n elements in the array.
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java index bc1a06e51f..3807e30d73 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java @@ -19,6 +19,7 @@ import static org.junit.Assert.*; import static org.springframework.data.mongodb.core.aggregation.AggregationFunctionExpressions.*; import static org.springframework.data.mongodb.core.aggregation.Fields.*; +import static org.springframework.data.mongodb.test.util.IsBsonObject.*; import static org.springframework.data.mongodb.util.DBObjectUtils.*; import java.util.Arrays; @@ -402,6 +403,90 @@ public void shouldRenderSliceWithPositionCorrectly() throws Exception { is((Object) new BasicDBObject("$slice", Arrays. asList("$field", 5, 10)))); } + /** + * @see DATAMONGO-784 + */ + @Test + public void shouldRenderCmpCorrectly() { + + ProjectionOperation operation = Aggregation.project().and("field").cmp(10).as("cmp10"); + + assertThat(operation.toDBObject(Aggregation.DEFAULT_CONTEXT), + isBsonObject().containing("$project.cmp10.$cmp.[0]", "$field").containing("$project.cmp10.$cmp.[1]", 10)); + } + + /** + * @see DATAMONGO-784 + */ + @Test + public void shouldRenderEqCorrectly() { + + ProjectionOperation operation = Aggregation.project().and("field").eq(10).as("eq10"); + + assertThat(operation.toDBObject(Aggregation.DEFAULT_CONTEXT), + isBsonObject().containing("$project.eq10.$eq.[0]", "$field").containing("$project.eq10.$eq.[1]", 10)); + } + + /** + * @see DATAMONGO-784 + */ + @Test + public void shouldRenderGtCorrectly() { + + ProjectionOperation operation = Aggregation.project().and("field").gt(10).as("gt10"); + + assertThat(operation.toDBObject(Aggregation.DEFAULT_CONTEXT), + isBsonObject().containing("$project.gt10.$gt.[0]", "$field").containing("$project.gt10.$gt.[1]", 10)); + } + + /** + * @see DATAMONGO-784 + */ + @Test + public void shouldRenderGteCorrectly() { + + ProjectionOperation operation = Aggregation.project().and("field").gte(10).as("gte10"); + + assertThat(operation.toDBObject(Aggregation.DEFAULT_CONTEXT), + isBsonObject().containing("$project.gte10.$gte.[0]", "$field").containing("$project.gte10.$gte.[1]", 10)); + } + + /** + * @see DATAMONGO-784 + */ + @Test + public void shouldRenderLtCorrectly() { + + ProjectionOperation operation = Aggregation.project().and("field").lt(10).as("lt10"); + + assertThat(operation.toDBObject(Aggregation.DEFAULT_CONTEXT), + isBsonObject().containing("$project.lt10.$lt.[0]", "$field").containing("$project.lt10.$lt.[1]", 10)); + } + + /** + * @see DATAMONGO-784 + */ + @Test + public void shouldRenderLteCorrectly() { + + ProjectionOperation operation = Aggregation.project().and("field").lte(10).as("lte10"); + + assertThat(operation.toDBObject(Aggregation.DEFAULT_CONTEXT), + isBsonObject().containing("$project.lte10.$lte.[0]", "$field").containing("$project.lte10.$lte.[1]", 10)); + } + + /** + * @see DATAMONGO-784 + */ + @Test + public void shouldRenderNeCorrectly() { + + ProjectionOperation operation = Aggregation.project().and("field").ne(10).as("ne10"); + + assertThat(operation.toDBObject(Aggregation.DEFAULT_CONTEXT), + isBsonObject().containing("$project.ne10.$ne.[0]", "$field").containing("$project.ne10.$ne.[1]", 10)); + } + private static DBObject exctractOperation(String field, DBObject fromProjectClause) { return (DBObject) fromProjectClause.get(field); } From 710770e88de4a72d69d26565850c05b55fa3db5c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 24 Nov 2016 13:45:38 +0100 Subject: [PATCH 7/9] DATAMONGO-784 - Polishing. Add JavaDoc for compareValue. Original pull request: #414. --- .../data/mongodb/core/aggregation/ProjectionOperation.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 7b74aa8faf..2eaba433dd 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 @@ -619,6 +619,7 @@ public ProjectionOperationBuilder size() { /** * Generates a {@code $cmp} expression (compare to) that compares the value of the field to a given value or field. * + * @param compareValue compare value or a {@link Field} object. * @return never {@literal null}. * @since 1.10 */ @@ -629,6 +630,7 @@ public ProjectionOperationBuilder cmp(Object compareValue) { /** * Generates a {@code $eq} expression (equal) that compares the value of the field to a given value or field. * + * @param compareValue compare value or a {@link Field} object. * @return never {@literal null}. * @since 1.10 */ @@ -639,6 +641,7 @@ public ProjectionOperationBuilder eq(Object compareValue) { /** * Generates a {@code $gt} expression (greater than) that compares the value of the field to a given value or field. * + * @param compareValue compare value or a {@link Field} object. * @return never {@literal null}. * @since 1.10 */ @@ -650,6 +653,7 @@ public ProjectionOperationBuilder gt(Object compareValue) { * Generates a {@code $gte} expression (greater than equal) that compares the value of the field to a given value or * field. * + * @param compareValue compare value or a {@link Field} object. * @return never {@literal null}. * @since 1.10 */ @@ -660,6 +664,7 @@ public ProjectionOperationBuilder gte(Object compareValue) { /** * Generates a {@code $lt} expression (less than) that compares the value of the field to a given value or field. * + * @param compareValue compare value or a {@link Field} object. * @return never {@literal null}. * @since 1.10 */ @@ -671,6 +676,7 @@ public ProjectionOperationBuilder lt(Object compareValue) { * Generates a {@code $lte} expression (less than equal) that compares the value of the field to a given value or * field. * + * @param compareValue the compare value or a {@link Field} object. * @return never {@literal null}. * @since 1.10 */ @@ -681,6 +687,7 @@ public ProjectionOperationBuilder lte(Object compareValue) { /** * Generates a {@code $ne} expression (not equal) that compares the value of the field to a given value or field. * + * @param compareValue compare value or a {@link Field} object. * @return never {@literal null}. * @since 1.10 */ From b786b8220ad2d9b4cfbfdca888b747b48917c162 Mon Sep 17 00:00:00 2001 From: Sebastien Gerard Date: Wed, 16 Nov 2016 15:06:44 +0100 Subject: [PATCH 8/9] DATAMONGO-1530 - Add support for missing MongoDB 3.2 aggregation pipeline operators. Original Pull Request: #410 --- .../core/spel/MethodReferenceNode.java | 79 ++++++++++++++++--- .../data/mongodb/core/spel/OperatorNode.java | 8 +- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index a32c49c3c1..12dd51d0f8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -15,32 +15,89 @@ */ package org.springframework.data.mongodb.core.spel; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.ast.MethodReference; + import java.util.Collections; import java.util.HashMap; import java.util.Map; -import org.springframework.expression.spel.ExpressionState; -import org.springframework.expression.spel.ast.MethodReference; - /** * An {@link ExpressionNode} representing a method reference. - * + * * @author Oliver Gierke * @author Thomas Darimont + * @author Sebastien Gerard */ public class MethodReferenceNode extends ExpressionNode { private static final Map FUNCTIONS; static { - Map map = new HashMap(); + map.put("and", "$and"); // Returns true only when all its expressions evaluate to true. + map.put("or", "$or"); // Returns true when any of its expressions evaluates to true. + map.put("not", "$not"); // Returns the boolean value that is the opposite of its argument expression. + + map.put("setEquals", "$setEquals"); // Returns true if the input sets have the same distinct elements. + map.put("setIntersection", "$setIntersection"); // Returns a set with elements that appear in all of the input sets. + map.put("setUnion", "$setUnion"); // Returns a set with elements that appear in any of the input sets. + map.put("setDifference", "$setDifference"); // Returns a set with elements that appear in the 1st set but not in the + // 2nd. + map.put("setIsSubset", "$setIsSubset"); // Returns true if all elements of the 1st set appear in the 2nd set. + map.put("anyElementTrue", "$anyElementTrue"); // Returns whether any elements of a set evaluate to true. + map.put("allElementsTrue", "$allElementsTrue"); // Returns whether no element of a set evaluates to false. + + map.put("cmp", "$cmp"); // Returns: 0 if the two values are equivalent, 1 if the first value is greater than the + // second, and -1 if the first value is less than the second. + map.put("eq", "$eq"); // Returns true if the values are equivalent. + map.put("gt", "$gt"); // Returns true if the first value is greater than the second. + map.put("gte", "$gte"); // Returns true if the first value is greater than or equal to the second. + map.put("lt", "$lt"); // Returns true if the first value is less than the second. + map.put("lte", "$lte"); // Returns true if the first value is less than or equal to the second. + map.put("ne", "$ne"); // Returns true if the values are not equivalent. + + map.put("abs", "$abs"); // Returns the absolute value of a number.; + map.put("add", "$add"); // Adds numbers to return the sum, or adds numbers and a date to return a new date. + map.put("ceil", "$ceil"); // Returns the smallest integer greater than or equal to the specified number. + map.put("divide", "$divide"); // Returns the result of dividing the first number by the second. + map.put("exp", "$exp"); // Raises e to the specified exponent. + map.put("floor", "$floor"); // Returns the largest integer less than or equal to the specified number. + map.put("ln", "$ln"); // Calculates the natural log of a number. + map.put("log", "$log"); // Calculates the log of a number in the specified base. + map.put("log10", "$log10"); // Calculates the log base 10 of a number. + map.put("mod", "$mod"); // Returns the remainder of the first number divided by the second. + map.put("multiply", "$multiply"); // Multiplies numbers to return the product. + map.put("pow", "$pow"); // Raises a number to the specified exponent. + map.put("sqrt", "$sqrt"); // Calculates the square root. + map.put("subtract", "$subtract"); // Returns the result of subtracting the second value from the first. If the + // two values are numbers, return the difference. If the two values are dates, return the difference in + // milliseconds. + map.put("trunc", "$trunc"); // Truncates a number to its integer. + map.put("concat", "$concat"); // Concatenates two strings. - map.put("strcasecmp", "$strcasecmp"); // Compares two strings and returns an integer that reflects the comparison. map.put("substr", "$substr"); // Takes a string and returns portion of that string. map.put("toLower", "$toLower"); // Converts a string to lowercase. map.put("toUpper", "$toUpper"); // Converts a string to uppercase. + map.put("strcasecmp", "$strcasecmp"); // Compares two strings and returns an integer that reflects the comparison. + + map.put("meta", "$meta"); // Access text search metadata. + + map.put("arrayElemAt", "$arrayElemAt"); // Returns the element at the specified array index. + map.put("concatArrays", "$concatArrays"); // Concatenates arrays to return the concatenated array. + map.put("filter", "$filter"); // Selects a subset of the array to return an array with only the elements that + // match the filter condition. + map.put("isArray", "$isArray"); // Determines if the operand is an array. Returns a boolean. + map.put("size", "$size"); // Returns the number of elements in the array. + map.put("slice", "$slice"); // Returns a subset of an array. + + map.put("map", "$map"); // Applies a subexpression to each element of an array and returns the array of + // resulting values in order. + map.put("let", "$let"); // Defines variables for use within the scope of a subexpression and returns the result + // of the subexpression. + + map.put("literal", "$literal"); // Return a value without parsing. map.put("dayOfYear", "$dayOfYear"); // Converts a date to a number between 1 and 366. map.put("dayOfMonth", "$dayOfMonth"); // Converts a date to a number between 1 and 31. @@ -53,6 +110,13 @@ public class MethodReferenceNode extends ExpressionNode { map.put("second", "$second"); // Converts a date into a number between 0 and 59. May be 60 to account for leap // seconds. map.put("millisecond", "$millisecond"); // Returns the millisecond portion of a date as an integer between 0 and + // 999. + map.put("dateToString", "$dateToString"); // Returns the date as a formatted string. + + map.put("cond", "$cond"); // A ternary operator that evaluates one expression, and depending on the result, + // returns the value of one of the other two expressions. + map.put("ifNull", "$ifNull"); // Returns either the non-null result of the first expression or the result of the + // second expression if the first expression results in a null result. FUNCTIONS = Collections.unmodifiableMap(map); } @@ -63,11 +127,8 @@ public class MethodReferenceNode extends ExpressionNode { /** * Returns the name of the method. - * - * @return */ public String getMethodName() { - String name = getName(); String methodName = name.substring(0, name.indexOf('(')); return FUNCTIONS.get(methodName); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java index 55a11bd7ec..4f8d8f3d1e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java @@ -29,7 +29,7 @@ /** * An {@link ExpressionNode} representing an operator. - * + * * @author Oliver Gierke * @author Thomas Darimont */ @@ -47,6 +47,10 @@ public class OperatorNode extends ExpressionNode { map.put("/", "$divide"); map.put("%", "$mod"); + map.put("and", "and"); + map.put("or", "or"); + map.put("!", "not"); + OPERATORS = Collections.unmodifiableMap(map); } @@ -54,7 +58,7 @@ public class OperatorNode extends ExpressionNode { /** * Creates a new {@link OperatorNode} from the given {@link Operator} and {@link ExpressionState}. - * + * * @param node must not be {@literal null}. * @param state must not be {@literal null}. */ From a741400e9b1a369ffba8b54b862c1c13119d306f Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 21 Nov 2016 08:17:55 +0100 Subject: [PATCH 9/9] DATAMONGO-1530 - Polishing. Add missing transformations for ConstructorReference, OperatorNot, OpNE, OpEQ, OpGT, OpGE, OpLT, OpLE, OperatorPower, OpOr and OpAnd. This allows usage of logical operators &, || and ! as part of the expression, while ConstructorReference allows instantiating eg. arrays via an expression `new int[]{4,5,6}`. This can be useful eg. comparing arrays using $setEquals. More complex aggregation operators like $filter can be created by defining the variable references as string inside the expression like filter(a, 'num', '$$num' > 10). Commands like $let requires usage of InlineMap to pass in required arguments like eg. let({low:1, high:'$$low'}, gt('$$low', '$$high')). Original Pull Request: #410 --- .../SpelExpressionTransformer.java | 135 +++- .../mongodb/core/spel/ExpressionNode.java | 18 +- .../data/mongodb/core/spel/LiteralNode.java | 28 +- .../core/spel/MethodReferenceNode.java | 326 +++++--- .../mongodb/core/spel/NotOperatorNode.java | 45 ++ .../data/mongodb/core/spel/OperatorNode.java | 64 +- .../SpelExpressionTransformerUnitTests.java | 712 +++++++++++++++++- src/main/asciidoc/reference/mongodb.adoc | 43 ++ 8 files changed, 1248 insertions(+), 123 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/NotOperatorNode.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java index 58dc08b364..d381020b5c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -26,19 +26,26 @@ import org.springframework.data.mongodb.core.spel.ExpressionTransformationContextSupport; import org.springframework.data.mongodb.core.spel.LiteralNode; import org.springframework.data.mongodb.core.spel.MethodReferenceNode; +import org.springframework.data.mongodb.core.spel.MethodReferenceNode.AggregationMethodReference; +import org.springframework.data.mongodb.core.spel.MethodReferenceNode.AggregationMethodReference.ArgumentType; +import org.springframework.data.mongodb.core.spel.NotOperatorNode; import org.springframework.data.mongodb.core.spel.OperatorNode; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelNode; import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.expression.spel.ast.CompoundExpression; +import org.springframework.expression.spel.ast.ConstructorReference; import org.springframework.expression.spel.ast.Indexer; import org.springframework.expression.spel.ast.InlineList; +import org.springframework.expression.spel.ast.InlineMap; +import org.springframework.expression.spel.ast.OperatorNot; import org.springframework.expression.spel.ast.PropertyOrFieldReference; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; +import org.springframework.util.ObjectUtils; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; @@ -48,6 +55,7 @@ * Renders the AST of a SpEL expression as a MongoDB Aggregation Framework projection expression. * * @author Thomas Darimont + * @author Christoph Strobl */ class SpelExpressionTransformer implements AggregationExpressionTransformer { @@ -69,6 +77,8 @@ public SpelExpressionTransformer() { conversions.add(new PropertyOrFieldReferenceNodeConversion(this)); conversions.add(new CompoundExpressionNodeConversion(this)); conversions.add(new MethodReferenceNodeConversion(this)); + conversions.add(new NotOperatorNodeConversion(this)); + conversions.add(new ValueRetrievingNodeConversion(this)); this.conversions = Collections.unmodifiableList(conversions); } @@ -131,8 +141,8 @@ private ExpressionNodeConversion lookupConversionFor(ExpressionN * @author Thomas Darimont * @author Oliver Gierke */ - private static abstract class ExpressionNodeConversion implements - AggregationExpressionTransformer { + private static abstract class ExpressionNodeConversion + implements AggregationExpressionTransformer { private final AggregationExpressionTransformer transformer; private final Class nodeType; @@ -235,8 +245,17 @@ public OperatorNodeConversion(AggregationExpressionTransformer transformer) { protected Object convert(AggregationExpressionTransformationContext context) { OperatorNode currentNode = context.getCurrentNode(); - DBObject operationObject = createOperationObjectAndAddToPreviousArgumentsIfNecessary(context, currentNode); + + if (currentNode.isLogicalOperator()) { + + for (ExpressionNode expressionNode : currentNode) { + transform(expressionNode, currentNode, operationObject, context); + } + + return operationObject; + } + Object leftResult = transform(currentNode.getLeft(), currentNode, operationObject, context); if (currentNode.isUnaryMinus()) { @@ -271,7 +290,8 @@ private DBObject createOperationObjectAndAddToPreviousArgumentsIfNecessary( return nextDbObject; } - private Object convertUnaryMinusOp(ExpressionTransformationContextSupport context, Object leftResult) { + private Object convertUnaryMinusOp(ExpressionTransformationContextSupport context, + Object leftResult) { Object result = leftResult instanceof Number ? leftResult : new BasicDBObject("$multiply", dbList(-1, leftResult)); @@ -289,7 +309,7 @@ private Object convertUnaryMinusOp(ExpressionTransformationContextSupport context) { MethodReferenceNode node = context.getCurrentNode(); - List args = new ArrayList(); + AggregationMethodReference methodReference = node.getMethodReference(); - for (ExpressionNode childNode : node) { - args.add(transform(childNode, context)); + Object args = null; + + if (ObjectUtils.nullSafeEquals(methodReference.getArgumentType(), ArgumentType.SINGLE)) { + args = transform(node.getChild(0), context); + } else if (ObjectUtils.nullSafeEquals(methodReference.getArgumentType(), ArgumentType.MAP)) { + + DBObject dbo = new BasicDBObject(); + for (int i = 0; i < methodReference.getArgumentMap().length; i++) { + dbo.put(methodReference.getArgumentMap()[i], transform(node.getChild(i), context)); + } + args = dbo; + } else { + + List argList = new ArrayList(); + + for (ExpressionNode childNode : node) { + argList.add(transform(childNode, context)); + } + + args = dbList(argList.toArray()); } - return context.addToPreviousOrReturn(new BasicDBObject(node.getMethodName(), dbList(args.toArray()))); + return context.addToPreviousOrReturn(new BasicDBObject(methodReference.getMongoOperator(), args)); } } @@ -510,4 +548,81 @@ protected boolean supports(ExpressionNode node) { return node.isOfType(CompoundExpression.class); } } + + /** + * @author Christoph Strobl + * @since 1.10 + */ + static class NotOperatorNodeConversion extends ExpressionNodeConversion { + + /** + * Creates a new {@link ExpressionNodeConversion}. + * + * @param transformer must not be {@literal null}. + */ + public NotOperatorNodeConversion(AggregationExpressionTransformer transformer) { + super(transformer); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.ExpressionConversionContext) + */ + @Override + protected Object convert(AggregationExpressionTransformationContext context) { + + NotOperatorNode node = context.getCurrentNode(); + List args = new ArrayList(); + + for (ExpressionNode childNode : node) { + args.add(transform(childNode, context)); + } + + return context.addToPreviousOrReturn(new BasicDBObject(node.getMongoOperator(), dbList(args.toArray()))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.NodeConversion#supports(org.springframework.data.mongodb.core.spel.ExpressionNode) + */ + @Override + protected boolean supports(ExpressionNode node) { + return node.isOfType(OperatorNot.class); + } + } + + /** + * @author Christoph Strobl + * @since 1.10 + */ + static class ValueRetrievingNodeConversion extends ExpressionNodeConversion { + + /** + * Creates a new {@link ExpressionNodeConversion}. + * + * @param transformer must not be {@literal null}. + */ + public ValueRetrievingNodeConversion(AggregationExpressionTransformer transformer) { + super(transformer); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.SpelNodeWrapper#convertSpelNodeToMongoObjectExpression(org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.ExpressionConversionContext) + */ + @Override + protected Object convert(AggregationExpressionTransformationContext context) { + return context.getCurrentNode().getValue(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.SpelExpressionTransformer.NodeConversion#supports(org.springframework.data.mongodb.core.spel.ExpressionNode) + */ + @Override + protected boolean supports(ExpressionNode node) { + return node.isOfType(InlineMap.class) || node.isOfType(InlineList.class) + || node.isOfType(ConstructorReference.class); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java index b323b9cf3b..01c7c18100 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-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. @@ -23,12 +23,14 @@ import org.springframework.expression.spel.ast.Literal; import org.springframework.expression.spel.ast.MethodReference; import org.springframework.expression.spel.ast.Operator; +import org.springframework.expression.spel.ast.OperatorNot; import org.springframework.util.Assert; /** * A value object for nodes in an expression. Allows iterating ove potentially available child {@link ExpressionNode}s. * * @author Oliver Gierke + * @author Christoph Strobl */ public class ExpressionNode implements Iterable { @@ -79,6 +81,10 @@ public static ExpressionNode from(SpelNode node, ExpressionState state) { return new LiteralNode((Literal) node, state); } + if (node instanceof OperatorNot) { + return new NotOperatorNode((OperatorNot) node, state); + } + return new ExpressionNode(node, state); } @@ -122,6 +128,16 @@ public boolean isMathematicalOperation() { return false; } + /** + * Returns whether the {@link ExpressionNode} is a logical conjunction operation like {@code &&, ||}. + * + * @return + * @since 1.10 + */ + public boolean isLogicalOperator() { + return false; + } + /** * Returns whether the {@link ExpressionNode} is a literal. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java index 68c53860f3..1f55294524 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/LiteralNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-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. @@ -15,7 +15,12 @@ */ package org.springframework.data.mongodb.core.spel; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.ast.BooleanLiteral; import org.springframework.expression.spel.ast.FloatLiteral; import org.springframework.expression.spel.ast.IntLiteral; import org.springframework.expression.spel.ast.Literal; @@ -26,13 +31,29 @@ /** * A node representing a literal in an expression. - * + * * @author Oliver Gierke + * @author Christoph Strobl */ public class LiteralNode extends ExpressionNode { + private static final Set> SUPPORTED_LITERAL_TYPES; private final Literal literal; + static { + + Set> supportedTypes = new HashSet>(7, 1); + supportedTypes.add(BooleanLiteral.class); + supportedTypes.add(FloatLiteral.class); + supportedTypes.add(IntLiteral.class); + supportedTypes.add(LongLiteral.class); + supportedTypes.add(NullLiteral.class); + supportedTypes.add(RealLiteral.class); + supportedTypes.add(StringLiteral.class); + + SUPPORTED_LITERAL_TYPES = Collections.unmodifiableSet(supportedTypes); + } + /** * Creates a new {@link LiteralNode} from the given {@link Literal} and {@link ExpressionState}. * @@ -66,7 +87,6 @@ public boolean isUnaryMinus(ExpressionNode parent) { */ @Override public boolean isLiteral() { - return literal instanceof FloatLiteral || literal instanceof RealLiteral || literal instanceof IntLiteral - || literal instanceof LongLiteral || literal instanceof StringLiteral || literal instanceof NullLiteral; + return SUPPORTED_LITERAL_TYPES.contains(literal.getClass()); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java index 12dd51d0f8..18ffc5a441 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-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. @@ -15,108 +15,132 @@ */ package org.springframework.data.mongodb.core.spel; -import org.springframework.expression.spel.ExpressionState; -import org.springframework.expression.spel.ast.MethodReference; +import static org.springframework.data.mongodb.core.spel.MethodReferenceNode.AggregationMethodReference.*; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.ast.MethodReference; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + /** * An {@link ExpressionNode} representing a method reference. * * @author Oliver Gierke * @author Thomas Darimont * @author Sebastien Gerard + * @author Christoph Strobl */ public class MethodReferenceNode extends ExpressionNode { - private static final Map FUNCTIONS; + private static final Map FUNCTIONS; static { - Map map = new HashMap(); - map.put("and", "$and"); // Returns true only when all its expressions evaluate to true. - map.put("or", "$or"); // Returns true when any of its expressions evaluates to true. - map.put("not", "$not"); // Returns the boolean value that is the opposite of its argument expression. + Map map = new HashMap(); - map.put("setEquals", "$setEquals"); // Returns true if the input sets have the same distinct elements. - map.put("setIntersection", "$setIntersection"); // Returns a set with elements that appear in all of the input sets. - map.put("setUnion", "$setUnion"); // Returns a set with elements that appear in any of the input sets. - map.put("setDifference", "$setDifference"); // Returns a set with elements that appear in the 1st set but not in the + // BOOLEAN OPERATORS + map.put("and", arrayArgumentAggregationMethodReference().forOperator("$and")); + map.put("or", arrayArgumentAggregationMethodReference().forOperator("$or")); + map.put("not", arrayArgumentAggregationMethodReference().forOperator("$not")); + + // SET OPERATORS + map.put("setEquals", arrayArgumentAggregationMethodReference().forOperator("$setEquals")); + map.put("setIntersection", arrayArgumentAggregationMethodReference().forOperator("$setIntersection")); + map.put("setUnion", arrayArgumentAggregationMethodReference().forOperator("$setUnion")); + map.put("setDifference", arrayArgumentAggregationMethodReference().forOperator("$setDifference")); // 2nd. - map.put("setIsSubset", "$setIsSubset"); // Returns true if all elements of the 1st set appear in the 2nd set. - map.put("anyElementTrue", "$anyElementTrue"); // Returns whether any elements of a set evaluate to true. - map.put("allElementsTrue", "$allElementsTrue"); // Returns whether no element of a set evaluates to false. - - map.put("cmp", "$cmp"); // Returns: 0 if the two values are equivalent, 1 if the first value is greater than the - // second, and -1 if the first value is less than the second. - map.put("eq", "$eq"); // Returns true if the values are equivalent. - map.put("gt", "$gt"); // Returns true if the first value is greater than the second. - map.put("gte", "$gte"); // Returns true if the first value is greater than or equal to the second. - map.put("lt", "$lt"); // Returns true if the first value is less than the second. - map.put("lte", "$lte"); // Returns true if the first value is less than or equal to the second. - map.put("ne", "$ne"); // Returns true if the values are not equivalent. - - map.put("abs", "$abs"); // Returns the absolute value of a number.; - map.put("add", "$add"); // Adds numbers to return the sum, or adds numbers and a date to return a new date. - map.put("ceil", "$ceil"); // Returns the smallest integer greater than or equal to the specified number. - map.put("divide", "$divide"); // Returns the result of dividing the first number by the second. - map.put("exp", "$exp"); // Raises e to the specified exponent. - map.put("floor", "$floor"); // Returns the largest integer less than or equal to the specified number. - map.put("ln", "$ln"); // Calculates the natural log of a number. - map.put("log", "$log"); // Calculates the log of a number in the specified base. - map.put("log10", "$log10"); // Calculates the log base 10 of a number. - map.put("mod", "$mod"); // Returns the remainder of the first number divided by the second. - map.put("multiply", "$multiply"); // Multiplies numbers to return the product. - map.put("pow", "$pow"); // Raises a number to the specified exponent. - map.put("sqrt", "$sqrt"); // Calculates the square root. - map.put("subtract", "$subtract"); // Returns the result of subtracting the second value from the first. If the - // two values are numbers, return the difference. If the two values are dates, return the difference in - // milliseconds. - map.put("trunc", "$trunc"); // Truncates a number to its integer. - - map.put("concat", "$concat"); // Concatenates two strings. - map.put("substr", "$substr"); // Takes a string and returns portion of that string. - map.put("toLower", "$toLower"); // Converts a string to lowercase. - map.put("toUpper", "$toUpper"); // Converts a string to uppercase. - map.put("strcasecmp", "$strcasecmp"); // Compares two strings and returns an integer that reflects the comparison. - - map.put("meta", "$meta"); // Access text search metadata. - - map.put("arrayElemAt", "$arrayElemAt"); // Returns the element at the specified array index. - map.put("concatArrays", "$concatArrays"); // Concatenates arrays to return the concatenated array. - map.put("filter", "$filter"); // Selects a subset of the array to return an array with only the elements that - // match the filter condition. - map.put("isArray", "$isArray"); // Determines if the operand is an array. Returns a boolean. - map.put("size", "$size"); // Returns the number of elements in the array. - map.put("slice", "$slice"); // Returns a subset of an array. - - map.put("map", "$map"); // Applies a subexpression to each element of an array and returns the array of - // resulting values in order. - map.put("let", "$let"); // Defines variables for use within the scope of a subexpression and returns the result - // of the subexpression. - - map.put("literal", "$literal"); // Return a value without parsing. - - map.put("dayOfYear", "$dayOfYear"); // Converts a date to a number between 1 and 366. - map.put("dayOfMonth", "$dayOfMonth"); // Converts a date to a number between 1 and 31. - map.put("dayOfWeek", "$dayOfWeek"); // Converts a date to a number between 1 and 7. - map.put("year", "$year"); // Converts a date to the full year. - map.put("month", "$month"); // Converts a date into a number between 1 and 12. - map.put("week", "$week"); // Converts a date into a number between 0 and 53 - map.put("hour", "$hour"); // Converts a date into a number between 0 and 23. - map.put("minute", "$minute"); // Converts a date into a number between 0 and 59. - map.put("second", "$second"); // Converts a date into a number between 0 and 59. May be 60 to account for leap - // seconds. - map.put("millisecond", "$millisecond"); // Returns the millisecond portion of a date as an integer between 0 and - // 999. - map.put("dateToString", "$dateToString"); // Returns the date as a formatted string. - - map.put("cond", "$cond"); // A ternary operator that evaluates one expression, and depending on the result, - // returns the value of one of the other two expressions. - map.put("ifNull", "$ifNull"); // Returns either the non-null result of the first expression or the result of the - // second expression if the first expression results in a null result. + map.put("setIsSubset", arrayArgumentAggregationMethodReference().forOperator("$setIsSubset")); + map.put("anyElementTrue", arrayArgumentAggregationMethodReference().forOperator("$anyElementTrue")); + map.put("allElementsTrue", arrayArgumentAggregationMethodReference().forOperator("$allElementsTrue")); + + // COMPARISON OPERATORS + map.put("cmp", arrayArgumentAggregationMethodReference().forOperator("$cmp")); + map.put("eq", arrayArgumentAggregationMethodReference().forOperator("$eq")); + map.put("gt", arrayArgumentAggregationMethodReference().forOperator("$gt")); + map.put("gte", arrayArgumentAggregationMethodReference().forOperator("$gte")); + map.put("lt", arrayArgumentAggregationMethodReference().forOperator("$lt")); + map.put("lte", arrayArgumentAggregationMethodReference().forOperator("$lte")); + map.put("ne", arrayArgumentAggregationMethodReference().forOperator("$ne")); + + // ARITHMETIC OPERATORS + map.put("abs", singleArgumentAggregationMethodReference().forOperator("$abs")); + map.put("add", arrayArgumentAggregationMethodReference().forOperator("$add")); + map.put("ceil", singleArgumentAggregationMethodReference().forOperator("$ceil")); + map.put("divide", arrayArgumentAggregationMethodReference().forOperator("$divide")); + map.put("exp", singleArgumentAggregationMethodReference().forOperator("$exp")); + map.put("floor", singleArgumentAggregationMethodReference().forOperator("$floor")); + map.put("ln", singleArgumentAggregationMethodReference().forOperator("$ln")); + map.put("log", arrayArgumentAggregationMethodReference().forOperator("$log")); + map.put("log10", singleArgumentAggregationMethodReference().forOperator("$log10")); + map.put("mod", arrayArgumentAggregationMethodReference().forOperator("$mod")); + map.put("multiply", arrayArgumentAggregationMethodReference().forOperator("$multiply")); + map.put("pow", arrayArgumentAggregationMethodReference().forOperator("$pow")); + map.put("sqrt", singleArgumentAggregationMethodReference().forOperator("$sqrt")); + map.put("subtract", arrayArgumentAggregationMethodReference().forOperator("$subtract")); + map.put("trunc", singleArgumentAggregationMethodReference().forOperator("$trunc")); + + // STRING OPERATORS + map.put("concat", arrayArgumentAggregationMethodReference().forOperator("$concat")); + map.put("strcasecmp", arrayArgumentAggregationMethodReference().forOperator("$strcasecmp")); + map.put("substr", arrayArgumentAggregationMethodReference().forOperator("$substr")); + map.put("toLower", singleArgumentAggregationMethodReference().forOperator("$toLower")); + map.put("toUpper", singleArgumentAggregationMethodReference().forOperator("$toUpper")); + map.put("strcasecmp", arrayArgumentAggregationMethodReference().forOperator("$strcasecmp")); + + // TEXT SEARCH OPERATORS + map.put("meta", singleArgumentAggregationMethodReference().forOperator("$meta")); + + // ARRAY OPERATORS + map.put("arrayElemAt", arrayArgumentAggregationMethodReference().forOperator("$arrayElemAt")); + map.put("concatArrays", arrayArgumentAggregationMethodReference().forOperator("$concatArrays")); + map.put("filter", mapArgumentAggregationMethodReference().forOperator("$filter") // + .mappingParametersTo("input", "as", "cond")); + map.put("isArray", singleArgumentAggregationMethodReference().forOperator("$isArray")); + map.put("size", singleArgumentAggregationMethodReference().forOperator("$size")); + map.put("slice", arrayArgumentAggregationMethodReference().forOperator("$slice")); + + // VARIABLE OPERATORS + map.put("map", mapArgumentAggregationMethodReference().forOperator("$map") // + .mappingParametersTo("input", "as", "in")); + map.put("let", mapArgumentAggregationMethodReference().forOperator("$let").mappingParametersTo("vars", "in")); + + // LITERAL OPERATORS + map.put("literal", singleArgumentAggregationMethodReference().forOperator("$literal")); + + // DATE OPERATORS + map.put("dayOfYear", singleArgumentAggregationMethodReference().forOperator("$dayOfYear")); + map.put("dayOfMonth", singleArgumentAggregationMethodReference().forOperator("$dayOfMonth")); + map.put("dayOfWeek", singleArgumentAggregationMethodReference().forOperator("$dayOfWeek")); + map.put("year", singleArgumentAggregationMethodReference().forOperator("$year")); + map.put("month", singleArgumentAggregationMethodReference().forOperator("$month")); + map.put("week", singleArgumentAggregationMethodReference().forOperator("$week")); + map.put("hour", singleArgumentAggregationMethodReference().forOperator("$hour")); + map.put("minute", singleArgumentAggregationMethodReference().forOperator("$minute")); + map.put("second", singleArgumentAggregationMethodReference().forOperator("$second")); + map.put("millisecond", singleArgumentAggregationMethodReference().forOperator("$millisecond")); + map.put("dateToString", mapArgumentAggregationMethodReference().forOperator("$dateToString") // + .mappingParametersTo("format", "date")); + + // CONDITIONAL OPERATORS + map.put("cond", mapArgumentAggregationMethodReference().forOperator("$cond") // + .mappingParametersTo("if", "then", "else")); + map.put("ifNull", arrayArgumentAggregationMethodReference().forOperator("$ifNull")); + + // GROUP OPERATORS + map.put("sum", arrayArgumentAggregationMethodReference().forOperator("$sum")); + map.put("avg", arrayArgumentAggregationMethodReference().forOperator("$avg")); + map.put("first", singleArgumentAggregationMethodReference().forOperator("$first")); + map.put("last", singleArgumentAggregationMethodReference().forOperator("$last")); + map.put("max", arrayArgumentAggregationMethodReference().forOperator("$max")); + map.put("min", arrayArgumentAggregationMethodReference().forOperator("$min")); + map.put("push", singleArgumentAggregationMethodReference().forOperator("$push")); + map.put("addToSet", singleArgumentAggregationMethodReference().forOperator("$addToSet")); + map.put("stdDevPop", arrayArgumentAggregationMethodReference().forOperator("$stdDevPop")); + map.put("stdDevSamp", arrayArgumentAggregationMethodReference().forOperator("$stdDevSamp")); FUNCTIONS = Collections.unmodifiableMap(map); } @@ -127,10 +151,144 @@ public class MethodReferenceNode extends ExpressionNode { /** * Returns the name of the method. + * + * @Deprecated since 1.10. Please use {@link #getMethodReference()}. */ + @Deprecated public String getMethodName() { + + AggregationMethodReference methodReference = getMethodReference(); + return methodReference != null ? methodReference.getMongoOperator() : null; + } + + /** + * Return the {@link AggregationMethodReference}. + * + * @return can be {@literal null}. + * @since 1.10 + */ + public AggregationMethodReference getMethodReference() { + String name = getName(); String methodName = name.substring(0, name.indexOf('(')); return FUNCTIONS.get(methodName); } + + /** + * @author Christoph Strobl + * @since 1.10 + */ + public static final class AggregationMethodReference { + + private final String mongoOperator; + private final ArgumentType argumentType; + private final String[] argumentMap; + + /** + * Creates new {@link AggregationMethodReference}. + * + * @param mongoOperator can be {@literal null}. + * @param argumentType can be {@literal null}. + * @param argumentMap can be {@literal null}. + */ + private AggregationMethodReference(String mongoOperator, ArgumentType argumentType, String[] argumentMap) { + + this.mongoOperator = mongoOperator; + this.argumentType = argumentType; + this.argumentMap = argumentMap; + } + + /** + * Get the MongoDB specific operator. + * + * @return can be {@literal null}. + */ + public String getMongoOperator() { + return this.mongoOperator; + } + + /** + * Get the {@link ArgumentType} used by the MongoDB. + * + * @return never {@literal null}. + */ + public ArgumentType getArgumentType() { + return this.argumentType; + } + + /** + * Get the property names in order order of appearance in resulting operation. + * + * @return never {@literal null}. + */ + public String[] getArgumentMap() { + return argumentMap != null ? argumentMap : new String[] {}; + } + + /** + * Create a new {@link AggregationMethodReference} for a {@link ArgumentType#SINGLE} argument. + * + * @return never {@literal null}. + */ + static AggregationMethodReference singleArgumentAggregationMethodReference() { + return new AggregationMethodReference(null, ArgumentType.SINGLE, null); + } + + /** + * Create a new {@link AggregationMethodReference} for an {@link ArgumentType#ARRAY} argument. + * + * @return never {@literal null}. + */ + static AggregationMethodReference arrayArgumentAggregationMethodReference() { + return new AggregationMethodReference(null, ArgumentType.ARRAY, null); + } + + /** + * Create a new {@link AggregationMethodReference} for a {@link ArgumentType#MAP} argument. + * + * @return never {@literal null}. + */ + static AggregationMethodReference mapArgumentAggregationMethodReference() { + return new AggregationMethodReference(null, ArgumentType.MAP, null); + } + + /** + * Create a new {@link AggregationMethodReference} for a given {@literal aggregationExpressionOperator} reusing + * previously set arguments. + * + * @param aggregationExpressionOperator should not be {@literal null}. + * @return never {@literal null}. + */ + AggregationMethodReference forOperator(String aggregationExpressionOperator) { + return new AggregationMethodReference(aggregationExpressionOperator, argumentType, argumentMap); + } + + /** + * Create a new {@link AggregationMethodReference} for mapping actual parameters within the AST to the given + * {@literal aggregationExpressionProperties} reusing previously set arguments.
+ * NOTE: Can only be applied to {@link AggregationMethodReference} of type + * {@link ArgumentType#MAP}. + * + * @param aggregationExpressionProperties should not be {@literal null}. + * @return never {@literal null}. + * @throws IllegalArgumentException + */ + AggregationMethodReference mappingParametersTo(String... aggregationExpressionProperties) { + + Assert.isTrue(ObjectUtils.nullSafeEquals(argumentType, ArgumentType.MAP), + "Parameter mapping can only be applied to AggregationMethodReference with MAPPED ArgumentType."); + return new AggregationMethodReference(mongoOperator, argumentType, aggregationExpressionProperties); + } + + /** + * The actual argument type to use when mapping parameters to MongoDB specific format. + * + * @author Christoph Strobl + * @since 1.10 + */ + public enum ArgumentType { + SINGLE, ARRAY, MAP + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/NotOperatorNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/NotOperatorNode.java new file mode 100644 index 0000000000..1809307f51 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/NotOperatorNode.java @@ -0,0 +1,45 @@ +/* + * 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.spel; + +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.ast.OperatorNot; + +/** + * @author Christoph Strobl + * @since 1.10 + */ +public class NotOperatorNode extends ExpressionNode { + + private final OperatorNot operatorNode; + + /** + * Creates a new {@link ExpressionNode} from the given {@link OperatorNot} and {@link ExpressionState}. + * + * @param node must not be {@literal null}. + * @param state must not be {@literal null}. + */ + protected NotOperatorNode(OperatorNot node, ExpressionState state) { + + super(node, state); + this.operatorNode = node; + } + + public String getMongoOperator() { + return "$not"; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java index 4f8d8f3d1e..948c9544fe 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/OperatorNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-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. @@ -17,41 +17,76 @@ import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.ast.OpAnd; import org.springframework.expression.spel.ast.OpDivide; +import org.springframework.expression.spel.ast.OpEQ; +import org.springframework.expression.spel.ast.OpGE; +import org.springframework.expression.spel.ast.OpGT; +import org.springframework.expression.spel.ast.OpLE; +import org.springframework.expression.spel.ast.OpLT; import org.springframework.expression.spel.ast.OpMinus; import org.springframework.expression.spel.ast.OpModulus; import org.springframework.expression.spel.ast.OpMultiply; +import org.springframework.expression.spel.ast.OpNE; +import org.springframework.expression.spel.ast.OpOr; import org.springframework.expression.spel.ast.OpPlus; import org.springframework.expression.spel.ast.Operator; +import org.springframework.expression.spel.ast.OperatorPower; /** * An {@link ExpressionNode} representing an operator. * * @author Oliver Gierke * @author Thomas Darimont + * @author Christoph Strobl */ public class OperatorNode extends ExpressionNode { private static final Map OPERATORS; + private static final Set SUPPORTED_MATH_OPERATORS; static { - Map map = new HashMap(6); + Map map = new HashMap(14, 1); map.put("+", "$add"); map.put("-", "$subtract"); map.put("*", "$multiply"); map.put("/", "$divide"); map.put("%", "$mod"); + map.put("^", "$pow"); + map.put("==", "$eq"); + map.put("!=", "$ne"); + map.put(">", "$gt"); + map.put(">=", "$gte"); + map.put("<", "$lt"); + map.put("<=", "$lte"); - map.put("and", "and"); - map.put("or", "or"); - map.put("!", "not"); + map.put("and", "$and"); + map.put("or", "$or"); OPERATORS = Collections.unmodifiableMap(map); + + Set set = new HashSet(12, 1); + set.add(OpMinus.class); + set.add(OpPlus.class); + set.add(OpMultiply.class); + set.add(OpDivide.class); + set.add(OpModulus.class); + set.add(OperatorPower.class); + set.add(OpNE.class); + set.add(OpEQ.class); + set.add(OpGT.class); + set.add(OpGE.class); + set.add(OpLT.class); + set.add(OpLE.class); + + SUPPORTED_MATH_OPERATORS = Collections.unmodifiableSet(set); } private final Operator operator; @@ -73,8 +108,16 @@ public class OperatorNode extends ExpressionNode { */ @Override public boolean isMathematicalOperation() { - return operator instanceof OpMinus || operator instanceof OpPlus || operator instanceof OpMultiply - || operator instanceof OpDivide || operator instanceof OpModulus; + return SUPPORTED_MATH_OPERATORS.contains(operator.getClass()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.spel.ExpressionNode#isConjunctionOperator() + */ + @Override + public boolean isLogicalOperator() { + return operator instanceof OpOr || operator instanceof OpAnd; } /** @@ -92,6 +135,13 @@ public boolean isUnaryOperator() { * @return */ public String getMongoOperator() { + + if (!OPERATORS.containsKey(operator.getOperatorName())) { + throw new IllegalArgumentException(String.format( + "Unknown operator name. Cannot translate %s into its MongoDB aggregation function representation.", + operator.getOperatorName())); + } + return OPERATORS.get(operator.getOperatorName()); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java index ad72d97fa4..770145a80a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -29,6 +29,7 @@ * @see DATAMONGO-774 * @author Thomas Darimont * @author Oliver Gierke + * @author Christoph Strobl */ public class SpelExpressionTransformerUnitTests { @@ -69,7 +70,7 @@ public void shouldSupportKnownOperands() { @Test(expected = IllegalArgumentException.class) public void shouldThrowExceptionOnUnknownOperand() { - transform("a ^ 1"); + transform("a++"); } @Test @@ -80,17 +81,15 @@ public void shouldRenderSumExpression() { @Test public void shouldRenderFormula() { - assertThat( - transform("(netPrice + surCharge) * taxrate + 42"), - is("{ \"$add\" : [ { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\"]} , 42]}")); + assertThat(transform("(netPrice + surCharge) * taxrate + 42"), is( + "{ \"$add\" : [ { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\"]} , 42]}")); } @Test public void shouldRenderFormulaInCurlyBrackets() { - assertThat( - transform("{(netPrice + surCharge) * taxrate + 42}"), - is("{ \"$add\" : [ { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\"]} , 42]}")); + assertThat(transform("{(netPrice + surCharge) * taxrate + 42}"), is( + "{ \"$add\" : [ { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\"]} , 42]}")); } @Test @@ -136,9 +135,8 @@ public void shouldRenderComplexExpression1() { @Test public void shouldRenderComplexExpression2() { - assertThat( - transform("(q + 1 + 4 - 5) / (q + 1 + 3 + 4)"), - is("{ \"$divide\" : [ { \"$subtract\" : [ { \"$add\" : [ \"$q\" , 1 , 4]} , 5]} , { \"$add\" : [ \"$q\" , 1 , 3 , 4]}]}")); + assertThat(transform("(q + 1 + 4 - 5) / (q + 1 + 3 + 4)"), is( + "{ \"$divide\" : [ { \"$subtract\" : [ { \"$add\" : [ \"$q\" , 1 , 4]} , 5]} , { \"$add\" : [ \"$q\" , 1 , 3 , 4]}]}")); } @Test @@ -195,15 +193,695 @@ public void shouldRenderCompoundExpressionsWithOnlyFieldReferences() { assertThat(transform("a.b + a.c"), is("{ \"$add\" : [ \"$a.b\" , \"$a.c\"]}")); } + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeAnd() { + assertThat(transform("and(a, b)"), is("{ \"$and\" : [ \"$a\" , \"$b\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeOr() { + assertThat(transform("or(a, b)"), is("{ \"$or\" : [ \"$a\" , \"$b\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeNot() { + assertThat(transform("not(a)"), is("{ \"$not\" : [ \"$a\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeSetEquals() { + assertThat(transform("setEquals(a, b)"), is("{ \"$setEquals\" : [ \"$a\" , \"$b\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeSetEqualsForArrays() { + assertThat(transform("setEquals(new int[]{1,2,3}, new int[]{4,5,6})"), + is("{ \"$setEquals\" : [ [ 1 , 2 , 3] , [ 4 , 5 , 6]]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeSetEqualsMixedArrays() { + assertThat(transform("setEquals(a, new int[]{4,5,6})"), is("{ \"$setEquals\" : [ \"$a\" , [ 4 , 5 , 6]]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceSetIntersection() { + assertThat(transform("setIntersection(a, new int[]{4,5,6})"), + is("{ \"$setIntersection\" : [ \"$a\" , [ 4 , 5 , 6]]}")); + } + + /** + * @see DATAMONGO-1530 + */ @Test - public void shouldRenderStringFunctions() { + public void shouldRenderMethodReferenceSetUnion() { + assertThat(transform("setUnion(a, new int[]{4,5,6})"), is("{ \"$setUnion\" : [ \"$a\" , [ 4 , 5 , 6]]}")); + } - assertThat(transform("concat(a, b)"), is("{ \"$concat\" : [ \"$a\" , \"$b\"]}")); - assertThat(transform("substr(a, 1, 2)"), is("{ \"$substr\" : [ \"$a\" , 1 , 2]}")); + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceSeDifference() { + assertThat(transform("setDifference(a, new int[]{4,5,6})"), is("{ \"$setDifference\" : [ \"$a\" , [ 4 , 5 , 6]]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceSetIsSubset() { + assertThat(transform("setIsSubset(a, new int[]{4,5,6})"), is("{ \"$setIsSubset\" : [ \"$a\" , [ 4 , 5 , 6]]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceAnyElementTrue() { + assertThat(transform("anyElementTrue(a)"), is("{ \"$anyElementTrue\" : [ \"$a\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceAllElementsTrue() { + assertThat(transform("allElementsTrue(a, new int[]{4,5,6})"), + is("{ \"$allElementsTrue\" : [ \"$a\" , [ 4 , 5 , 6]]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceCmp() { + assertThat(transform("cmp(a, 250)"), is("{ \"$cmp\" : [ \"$a\" , 250]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceEq() { + assertThat(transform("eq(a, 250)"), is("{ \"$eq\" : [ \"$a\" , 250]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceGt() { + assertThat(transform("gt(a, 250)"), is("{ \"$gt\" : [ \"$a\" , 250]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceGte() { + assertThat(transform("gte(a, 250)"), is("{ \"$gte\" : [ \"$a\" , 250]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceLt() { + assertThat(transform("lt(a, 250)"), is("{ \"$lt\" : [ \"$a\" , 250]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceLte() { + assertThat(transform("lte(a, 250)"), is("{ \"$lte\" : [ \"$a\" , 250]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNe() { + assertThat(transform("ne(a, 250)"), is("{ \"$ne\" : [ \"$a\" , 250]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceAbs() { + assertThat(transform("abs(1)"), is("{ \"$abs\" : 1}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceAdd() { + assertThat(transform("add(a, 250)"), is("{ \"$add\" : [ \"$a\" , 250]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceCeil() { + assertThat(transform("ceil(7.8)"), is("{ \"$ceil\" : 7.8}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceDivide() { + assertThat(transform("divide(a, 250)"), is("{ \"$divide\" : [ \"$a\" , 250]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceExp() { + assertThat(transform("exp(2)"), is("{ \"$exp\" : 2}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceFloor() { + assertThat(transform("floor(2)"), is("{ \"$floor\" : 2}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceLn() { + assertThat(transform("ln(2)"), is("{ \"$ln\" : 2}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceLog() { + assertThat(transform("log(100, 10)"), is("{ \"$log\" : [ 100 , 10]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceLog10() { + assertThat(transform("log10(100)"), is("{ \"$log10\" : 100}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeMod() { + assertThat(transform("mod(a, b)"), is("{ \"$mod\" : [ \"$a\" , \"$b\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeMultiply() { + assertThat(transform("multiply(a, b)"), is("{ \"$multiply\" : [ \"$a\" , \"$b\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodePow() { + assertThat(transform("pow(a, 2)"), is("{ \"$pow\" : [ \"$a\" , 2]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceSqrt() { + assertThat(transform("sqrt(2)"), is("{ \"$sqrt\" : 2}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeSubtract() { + assertThat(transform("subtract(a, b)"), is("{ \"$subtract\" : [ \"$a\" , \"$b\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceTrunc() { + assertThat(transform("trunc(2.1)"), is("{ \"$trunc\" : 2.1}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeConcat() { + assertThat(transform("concat(a, b, 'c')"), is("{ \"$concat\" : [ \"$a\" , \"$b\" , \"c\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeSubstrc() { + assertThat(transform("substr(a, 0, 1)"), is("{ \"$substr\" : [ \"$a\" , 0 , 1]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceToLower() { + assertThat(transform("toLower(a)"), is("{ \"$toLower\" : \"$a\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceToUpper() { + assertThat(transform("toUpper(a)"), is("{ \"$toUpper\" : \"$a\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeStrCaseCmp() { assertThat(transform("strcasecmp(a, b)"), is("{ \"$strcasecmp\" : [ \"$a\" , \"$b\"]}")); - assertThat(transform("toLower(a)"), is("{ \"$toLower\" : [ \"$a\"]}")); - assertThat(transform("toUpper(a)"), is("{ \"$toUpper\" : [ \"$a\"]}")); - assertThat(transform("toUpper(toLower(a))"), is("{ \"$toUpper\" : [ { \"$toLower\" : [ \"$a\"]}]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceMeta() { + assertThat(transform("meta('textScore')"), is("{ \"$meta\" : \"textScore\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeArrayElemAt() { + assertThat(transform("arrayElemAt(a, 10)"), is("{ \"$arrayElemAt\" : [ \"$a\" , 10]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeConcatArrays() { + assertThat(transform("concatArrays(a, b, c)"), is("{ \"$concatArrays\" : [ \"$a\" , \"$b\" , \"$c\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeFilter() { + assertThat(transform("filter(a, 'num', '$$num' > 10)"), + is("{ \"$filter\" : { \"input\" : \"$a\" , \"as\" : \"num\" , \"cond\" : { \"$gt\" : [ \"$$num\" , 10]}}}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceIsArray() { + assertThat(transform("isArray(a)"), is("{ \"$isArray\" : \"$a\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceIsSize() { + assertThat(transform("size(a)"), is("{ \"$size\" : \"$a\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeSlice() { + assertThat(transform("slice(a, 10)"), is("{ \"$slice\" : [ \"$a\" , 10]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeMap() { + assertThat(transform("map(quizzes, 'grade', '$$grade' + 2)"), is( + "{ \"$map\" : { \"input\" : \"$quizzes\" , \"as\" : \"grade\" , \"in\" : { \"$add\" : [ \"$$grade\" , 2]}}}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeLet() { + assertThat(transform("let({low:1, high:'$$low'}, gt('$$low', '$$high'))"), is( + "{ \"$let\" : { \"vars\" : { \"low\" : 1 , \"high\" : \"$$low\"} , \"in\" : { \"$gt\" : [ \"$$low\" , \"$$high\"]}}}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceLiteral() { + assertThat(transform("literal($1)"), is("{ \"$literal\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceDayOfYear() { + assertThat(transform("dayOfYear($1)"), is("{ \"$dayOfYear\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceDayOfMonth() { + assertThat(transform("dayOfMonth($1)"), is("{ \"$dayOfMonth\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceDayOfWeek() { + assertThat(transform("dayOfWeek($1)"), is("{ \"$dayOfWeek\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceYear() { + assertThat(transform("year($1)"), is("{ \"$year\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceMonth() { + assertThat(transform("month($1)"), is("{ \"$month\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceWeek() { + assertThat(transform("week($1)"), is("{ \"$week\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceHour() { + assertThat(transform("hour($1)"), is("{ \"$hour\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceMinute() { + assertThat(transform("minute($1)"), is("{ \"$minute\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceSecond() { + assertThat(transform("second($1)"), is("{ \"$second\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceMillisecond() { + assertThat(transform("millisecond($1)"), is("{ \"$millisecond\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceDateToString() { + assertThat(transform("dateToString('%Y-%m-%d', $date)"), + is("{ \"$dateToString\" : { \"format\" : \"%Y-%m-%d\" , \"date\" : \"$date\"}}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceCond() { + assertThat(transform("cond(qty > 250, 30, 20)"), + is("{ \"$cond\" : { \"if\" : { \"$gt\" : [ \"$qty\" , 250]} , \"then\" : 30 , \"else\" : 20}}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeIfNull() { + assertThat(transform("ifNull(a, 10)"), is("{ \"$ifNull\" : [ \"$a\" , 10]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeSum() { + assertThat(transform("sum(a, b)"), is("{ \"$sum\" : [ \"$a\" , \"$b\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeAvg() { + assertThat(transform("avg(a, b)"), is("{ \"$avg\" : [ \"$a\" , \"$b\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceFirst() { + assertThat(transform("first($1)"), is("{ \"$first\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceLast() { + assertThat(transform("last($1)"), is("{ \"$last\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeMax() { + assertThat(transform("max(a, b)"), is("{ \"$max\" : [ \"$a\" , \"$b\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeMin() { + assertThat(transform("min(a, b)"), is("{ \"$min\" : [ \"$a\" , \"$b\"]}")); + } + + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodePush() { + assertThat(transform("push({'item':'$item', 'quantity':'$qty'})"), is("{ \"$push\" : { \"item\" : \"$item\" , \"quantity\" : \"$qty\"}}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceAddToSet() { + assertThat(transform("addToSet($1)"), is("{ \"$addToSet\" : \"$1\"}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeStdDevPop() { + assertThat(transform("stdDevPop(scores.score)"), is("{ \"$stdDevPop\" : [ \"$scores.score\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderMethodReferenceNodeStdDevSamp() { + assertThat(transform("stdDevSamp(age)"), is("{ \"$stdDevSamp\" : [ \"$age\"]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderOperationNodeEq() { + assertThat(transform("foo == 10"), is("{ \"$eq\" : [ \"$foo\" , 10]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderOperationNodeNe() { + assertThat(transform("foo != 10"), is("{ \"$ne\" : [ \"$foo\" , 10]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderOperationNodeGt() { + assertThat(transform("foo > 10"), is("{ \"$gt\" : [ \"$foo\" , 10]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderOperationNodeGte() { + assertThat(transform("foo >= 10"), is("{ \"$gte\" : [ \"$foo\" , 10]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderOperationNodeLt() { + assertThat(transform("foo < 10"), is("{ \"$lt\" : [ \"$foo\" , 10]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderOperationNodeLte() { + assertThat(transform("foo <= 10"), is("{ \"$lte\" : [ \"$foo\" , 10]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderOperationNodePow() { + assertThat(transform("foo^2"), is("{ \"$pow\" : [ \"$foo\" , 2]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderOperationNodeOr() { + assertThat(transform("true || false"), is("{ \"$or\" : [ true , false]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderComplexOperationNodeOr() { + assertThat(transform("1+2 || concat(a, b) || true"), + is("{ \"$or\" : [ { \"$add\" : [ 1 , 2]} , { \"$concat\" : [ \"$a\" , \"$b\"]} , true]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderOperationNodeAnd() { + assertThat(transform("true && false"), is("{ \"$and\" : [ true , false]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderComplexOperationNodeAnd() { + assertThat(transform("1+2 && concat(a, b) && true"), + is("{ \"$and\" : [ { \"$add\" : [ 1 , 2]} , { \"$concat\" : [ \"$a\" , \"$b\"]} , true]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderNotCorrectly() { + assertThat(transform("!true"), is("{ \"$not\" : [ true]}")); + } + + /** + * @see DATAMONGO-1530 + */ + @Test + public void shouldRenderComplexNotCorrectly() { + assertThat(transform("!(foo > 10)"), is("{ \"$not\" : [ { \"$gt\" : [ \"$foo\" , 10]}]}")); } private String transform(String expression, Object... params) { diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 0dad55c133..c375facbba 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1745,6 +1745,49 @@ will be translated into the following projection expression part: Have a look at an example in more context in <> and <>. You can find more usage examples for supported SpEL expression constructs in `SpelExpressionTransformerUnitTests`. +.Supported SpEL transformations +[cols="2"] +|=== +| a == b +| { $eq : [$a, $b] } +| a != b +| { $ne : [$a , $b] } +| a > b +| { $gt : [$a, $b] } +| a >= b +| { $gte : [$a, $b] } +| a < b +| { $lt : [$a, $b] } +| a <= b +| { $lte : [$a, $b] } +| a + b +| { $add : [$a, $b] } +| a - b +| { $subtract : [$a, $b] } +| a * b +| { $multiply : [$a, $b] } +| a / b +| { $divide : [$a, $b] } +| a^b +| { $pow : [$a, $b] } +| a % b +| { $mod : [$a, $b] } +| a && b +| { $and : [$a, $b] } +| a \|\| b +| { $or : [$a, $b] } +| !a +| { $not : [$a] } +|=== + +Next to the transformations shown in <> it is possible to use standard SpEL operations like `new` to eg. create arrays and reference expressions via their name followed by the arguments to use in brackets. + +[source,java] +---- +// { $setEquals : [$a, [5, 8, 13] ] } +.andExpression("setEquals(a, new int[]{5, 8, 13})"); +---- + [[mongo.aggregation.examples]] ==== Aggregation Framework Examples