From 4166b88e00f28857bc3a43703f313d53d6e377d8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 8 Jul 2016 14:27:52 +0200 Subject: [PATCH 1/3] DATAMONGO-861 - Add support for $cond operator in aggregation operation. Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-cross-store/pom.xml | 4 ++-- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb-log4j/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index dfb64f9ba3..2807000a4c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-861-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index ae0a5d6c8f..9ef00665c2 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-861-SNAPSHOT ../pom.xml @@ -48,7 +48,7 @@ org.springframework.data spring-data-mongodb - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-861-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 2d02722262..3e6b81bc9d 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-861-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-log4j/pom.xml b/spring-data-mongodb-log4j/pom.xml index ee5e3336db..bddc5fdb6c 100644 --- a/spring-data-mongodb-log4j/pom.xml +++ b/spring-data-mongodb-log4j/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-861-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 8072d3f665..26eefbaa82 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 1.10.0.BUILD-SNAPSHOT + 1.10.0.DATAMONGO-861-SNAPSHOT ../pom.xml From f1b3f238aaf927372abe33c45b34ed5c9c82aecb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 8 Jul 2016 16:44:13 +0200 Subject: [PATCH 2/3] DATAMONGO-861 - Add support for $cond and $ifNull operators in aggregation operations. We now support $cond and $ifNull operators for projection and grouping operations. ConditionalOperator and IfNullOperators are AggregationExpressions that can be applied to transform or generate values during aggregation. TypedAggregation agg = newAggregation(InventoryItem.class, project().and("discount") .transform(ConditionalOperator.newBuilder().when(Criteria.where("qty").gte(250)) .then(30) .otherwise(20)) .and(ifNull("description", "Unspecified")).as("description") ); corresponds to { "$project": { "discount": { "$cond": { "if": { "$gte": [ "$qty", 250 ] }, "then": 30, "else": 20 } }, "description": { "$ifNull": [ "$description", "Unspecified"] } } } --- .../mongodb/core/aggregation/Aggregation.java | 62 +++ .../core/aggregation/ConditionalOperator.java | 381 ++++++++++++++++++ .../core/aggregation/IfNullOperator.java | 186 +++++++++ .../core/aggregation/ProjectionOperation.java | 47 ++- .../core/aggregation/AggregationTests.java | 275 +++++++++++++ .../aggregation/AggregationUnitTests.java | 177 ++++++++ .../ConditionalOperatorUnitTests.java | 210 ++++++++++ .../aggregation/IfNullOperatorUnitTests.java | 90 +++++ ...dAggregationOperationContextUnitTests.java | 58 ++- src/main/asciidoc/new-features.adoc | 1 + src/main/asciidoc/reference/mongodb.adoc | 47 +++ 11 files changed, 1527 insertions(+), 7 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperator.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/IfNullOperator.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperatorUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/IfNullOperatorUnitTests.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 6ebd8cdc3b..375e7e72a7 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 @@ -372,6 +372,68 @@ public static LookupOperation lookup(Field from, Field localField, Field foreign return new LookupOperation(from, localField, foreignField, as); } + /** + * Creates a new {@link IfNullOperator} for the given {@code field} and {@code replacement} value. + * + * @param field must not be {@literal null}. + * @param replacement must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 + */ + public static IfNullOperator ifNull(String field, Object replacement) { + return IfNullOperator.newBuilder().ifNull(field).thenReplaceWith(replacement); + } + + /** + * Creates a new {@link IfNullOperator} for the given {@link Field} and {@link Field} to obtain a value from. + * + * @param field must not be {@literal null}. + * @param replacement must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 + */ + public static IfNullOperator ifNull(Field field, Field replacement) { + return IfNullOperator.newBuilder().ifNull(field).thenReplaceWith(replacement); + } + + /** + * Creates a new {@link IfNullOperator} for the given {@link Field} and {@code replacement} value. + * + * @param field must not be {@literal null}. + * @param replacement must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 + */ + public static IfNullOperator ifNull(Field field, Object replacement) { + return IfNullOperator.newBuilder().ifNull(field).thenReplaceWith(replacement); + } + + /** + * Creates a new {@link ConditionalOperator} for the given {@link Field} that holds a {@literal boolean} value. + * + * @param booleanField must not be {@literal null}. + * @param then must not be {@literal null}. + * @param otherwise must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 + */ + public static ConditionalOperator conditional(Field booleanField, Object then, Object otherwise) { + return ConditionalOperator.newBuilder().when(booleanField).then(then).otherwise(otherwise); + } + + /** + * Creates a new {@link ConditionalOperator} for the given {@link Criteria}. + * + * @param criteria must not be {@literal null}. + * @param then must not be {@literal null}. + * @param otherwise must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 + */ + public static ConditionalOperator conditional(Criteria criteria, Object then, Object otherwise) { + return ConditionalOperator.newBuilder().when(criteria).then(then).otherwise(otherwise); + } + /** * Creates a new {@link Fields} instance for the given field names. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperator.java new file mode 100644 index 0000000000..3a0d5575d9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperator.java @@ -0,0 +1,381 @@ +/* + * 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.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.mongodb.core.query.CriteriaDefinition; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Encapsulates the aggregation framework {@code $cond} operator. A {@link ConditionalOperator} allows nested conditions + * {@code if-then[if-then-else]-else} using {@link Field}, {@link CriteriaDefinition} or a {@link DBObject custom} + * condition. Replacement values can be either {@link Field field references}, values of simple MongoDB types or values + * that can be converted to a simple MongoDB type. + * + * @see http://docs.mongodb.com/manual/reference/operator/aggregation/cond/ + * @author Mark Paluch + * @since 1.10 + */ +public class ConditionalOperator implements AggregationExpression { + + private final Object condition; + private final Object thenValue; + private final Object otherwiseValue; + + /** + * Creates a new {@link ConditionalOperator} for a given {@link Field} and {@code then}/{@code otherwise} values. + * + * @param condition must not be {@literal null}. + * @param thenValue must not be {@literal null}. + * @param otherwiseValue must not be {@literal null}. + */ + public ConditionalOperator(Field condition, Object thenValue, Object otherwiseValue) { + this((Object) condition, thenValue, otherwiseValue); + } + + /** + * Creates a new {@link ConditionalOperator} for a given {@link CriteriaDefinition} and {@code then}/{@code otherwise} + * values. + * + * @param condition must not be {@literal null}. + * @param thenValue must not be {@literal null}. + * @param otherwiseValue must not be {@literal null}. + */ + public ConditionalOperator(CriteriaDefinition condition, Object thenValue, Object otherwiseValue) { + this((Object) condition, thenValue, otherwiseValue); + } + + /** + * Creates a new {@link ConditionalOperator} for a given {@link DBObject criteria} and {@code then}/{@code otherwise} + * values. + * + * @param condition must not be {@literal null}. + * @param thenValue must not be {@literal null}. + * @param otherwiseValue must not be {@literal null}. + */ + public ConditionalOperator(DBObject condition, Object thenValue, Object otherwiseValue) { + this((Object) condition, thenValue, otherwiseValue); + } + + private ConditionalOperator(Object condition, Object thenValue, Object otherwiseValue) { + + Assert.notNull(condition, "Condition must not be null!"); + Assert.notNull(thenValue, "'Then value' must not be null!"); + Assert.notNull(otherwiseValue, "'Otherwise value' must not be null!"); + + assertNotBuilder(condition, "Condition"); + assertNotBuilder(thenValue, "'Then value'"); + assertNotBuilder(otherwiseValue, "'Otherwise value'"); + + this.condition = condition; + this.thenValue = thenValue; + this.otherwiseValue = otherwiseValue; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public DBObject toDbObject(AggregationOperationContext context) { + + BasicDBObject condObject = new BasicDBObject(); + + condObject.append("if", resolveCriteria(context, condition)); + condObject.append("then", resolveValue(context, thenValue)); + condObject.append("else", resolveValue(context, otherwiseValue)); + + return new BasicDBObject("$cond", condObject); + } + + private Object resolveValue(AggregationOperationContext context, Object value) { + + if (value instanceof DBObject || value instanceof Field) { + return resolve(context, value); + } + + if (value instanceof ConditionalOperator) { + return ((ConditionalOperator) value).toDbObject(context); + } + + DBObject toMap = context.getMappedObject(new BasicDBObject("$set", value)); + return toMap.get("$set"); + } + + private Object resolveCriteria(AggregationOperationContext context, Object value) { + + if (value instanceof DBObject || value instanceof Field) { + return resolve(context, value); + } + + if (value instanceof CriteriaDefinition) { + + DBObject mappedObject = context.getMappedObject(((CriteriaDefinition) value).getCriteriaObject()); + BasicDBList clauses = new BasicDBList(); + + clauses.addAll(getClauses(context, mappedObject)); + + if (clauses.size() == 1) { + return clauses.get(0); + } + + return clauses; + } + + throw new InvalidDataAccessApiUsageException( + String.format("Invalid value in condition. Supported: DBObject, Field references, Criteria, got: %s", value)); + } + + private BasicDBList getClauses(AggregationOperationContext context, DBObject mappedObject) { + + BasicDBList clauses = new BasicDBList(); + + for (String key : mappedObject.keySet()) { + + Object predicate = mappedObject.get(key); + clauses.addAll(getClauses(context, key, predicate)); + } + + return clauses; + } + + private BasicDBList getClauses(AggregationOperationContext context, String key, Object predicate) { + + BasicDBList clauses = new BasicDBList(); + + if (predicate instanceof BasicDBList) { + + BasicDBList args = new BasicDBList(); + for (Object clause : (BasicDBList) predicate) { + args.addAll(getClauses(context, (BasicDBObject) clause)); + } + + clauses.add(new BasicDBObject(key, args)); + + } else if (predicate instanceof DBObject) { + + DBObject nested = (DBObject) predicate; + + for (String s : nested.keySet()) { + + if (!isKeyword(s)) { + continue; + } + + BasicDBList args = new BasicDBList(); + args.add("$" + key); + args.add(nested.get(s)); + clauses.add(new BasicDBObject(s, args)); + } + + } else if (!isKeyword(key)) { + + BasicDBList args = new BasicDBList(); + args.add("$" + key); + args.add(predicate); + clauses.add(new BasicDBObject("$eq", args)); + } + + return clauses; + } + + /** + * Returns whether the given {@link String} is a MongoDB keyword. + * + * @param candidate + * @return + */ + private boolean isKeyword(String candidate) { + return candidate.startsWith("$"); + } + + private Object resolve(AggregationOperationContext context, Object value) { + + if (value instanceof DBObject) { + return context.getMappedObject((DBObject) value); + } + + return context.getReference((Field) value).toString(); + } + + private void assertNotBuilder(Object toCheck, String name) { + Assert.isTrue(!ClassUtils.isAssignableValue(ConditionalExpressionBuilder.class, toCheck), + String.format("%s must not be of type %s", name, ConditionalExpressionBuilder.class.getSimpleName())); + } + + /** + * Get a builder that allows fluent creation of {@link ConditionalOperator}. + * + * @return a new {@link ConditionalExpressionBuilder}. + */ + public static ConditionalExpressionBuilder newBuilder() { + return ConditionalExpressionBuilder.newBuilder(); + } + + public static interface WhenBuilder { + + /** + * @param booleanExpression expression that yields in a boolean result, must not be {@literal null}. + * @return the {@link ThenBuilder} + */ + ThenBuilder when(DBObject booleanExpression); + + /** + * @param booleanField reference to a field holding a boolean value, must not be {@literal null}. + * @return the {@link ThenBuilder} + */ + ThenBuilder when(Field booleanField); + + /** + * @param booleanField name of a field holding a boolean value, must not be {@literal null}. + * @return the {@link ThenBuilder} + */ + ThenBuilder when(String booleanField); + + /** + * @param criteria criteria to evaluate, must not be {@literal null}. + * @return the {@link ThenBuilder} + */ + ThenBuilder when(CriteriaDefinition criteria); + } + + public static interface ThenBuilder { + + /** + * @param value the value to be used if the condition evaluates {@literal true}. Can be a {@link DBObject}, a value + * that is supported by MongoDB or a value that can be converted to a MongoDB representation but must not + * be {@literal null}. + * @return the {@link OtherwiseBuilder} + */ + OtherwiseBuilder then(Object value); + } + + public static interface OtherwiseBuilder { + + /** + * @param value the value to be used if the condition evaluates {@literal false}. Can be a {@link DBObject}, a value + * that is supported by MongoDB or a value that can be converted to a MongoDB representation but must not + * be {@literal null}. + * @return the {@link ConditionalOperator} + */ + ConditionalOperator otherwise(Object value); + } + + /** + * Builder for fluent {@link ConditionalOperator} creation. + * + * @author Mark Paluch + * @since 1.10 + */ + public static final class ConditionalExpressionBuilder implements WhenBuilder, ThenBuilder, OtherwiseBuilder { + + private Object condition; + private Object thenValue; + + private ConditionalExpressionBuilder() {} + + /** + * Creates a new builder for {@link ConditionalOperator}. + * + * @return never {@literal null}. + */ + public static ConditionalExpressionBuilder newBuilder() { + return new ConditionalExpressionBuilder(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.WhenBuilder#when(com.mongodb.DBObject) + */ + @Override + public ConditionalExpressionBuilder when(DBObject booleanExpression) { + + Assert.notNull(booleanExpression, "'Boolean expression' must not be null!"); + + this.condition = booleanExpression; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.WhenBuilder#when(org.springframework.data.mongodb.core.query.CriteriaDefinition) + */ + @Override + public ThenBuilder when(CriteriaDefinition criteria) { + + Assert.notNull(criteria, "Criteria must not be null!"); + + this.condition = criteria; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.WhenBuilder#when(org.springframework.data.mongodb.core.aggregation.Field) + */ + @Override + public ThenBuilder when(Field booleanField) { + + Assert.notNull(booleanField, "Boolean field must not be null!"); + + this.condition = booleanField; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.WhenBuilder#when(java.lang.String) + */ + @Override + public ThenBuilder when(String booleanField) { + + Assert.hasText(booleanField, "Boolean field name must not be null or empty!"); + + this.condition = Fields.field(booleanField); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.ThenBuilder#then(java.lang.Object) + */ + @Override + public OtherwiseBuilder then(Object thenValue) { + + Assert.notNull(thenValue, "'Then-value' must not be null!"); + + this.thenValue = thenValue; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.OtherwiseBuilder#otherwise(java.lang.Object) + */ + @Override + public ConditionalOperator otherwise(Object otherwiseValue) { + + Assert.notNull(otherwiseValue, "'Otherwise-value' must not be null!"); + + return new ConditionalOperator(condition, thenValue, otherwiseValue); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/IfNullOperator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/IfNullOperator.java new file mode 100644 index 0000000000..bf3a59af23 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/IfNullOperator.java @@ -0,0 +1,186 @@ +/* + * 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.util.Assert; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Encapsulates the aggregation framework {@code $ifNull} operator. Replacement values can be either {@link Field field + * references}, values of simple MongoDB types or values that can be converted to a simple MongoDB type. + * + * @see http://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/ + * @author Mark Paluch + * @since 1.10 + */ +public class IfNullOperator implements AggregationExpression { + + private final Field field; + private final Object value; + + /** + * Creates a new {@link IfNullOperator} for the given {@link Field} and replacement {@code value}. + * + * @param field must not be {@literal null}. + * @param value must not be {@literal null}. + */ + public IfNullOperator(Field field, Object value) { + + Assert.notNull(field, "Field must not be null!"); + Assert.notNull(value, "'Replacement-value' must not be null!"); + + this.field = field; + this.value = value; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public DBObject toDbObject(AggregationOperationContext context) { + + BasicDBList list = new BasicDBList(); + + list.add(context.getReference(field).toString()); + + if (value instanceof Field) { + list.add(context.getReference((Field) value).toString()); + } else { + + DBObject toMap = context.getMappedObject(new BasicDBObject("$set", value)); + list.add(toMap.get("$set")); + } + + return new BasicDBObject("$ifNull", list); + } + + /** + * Get a builder that allows fluent creation of {@link IfNullOperator}. + * + * @return a new {@link IfNullBuilder}. + */ + public static IfNullBuilder newBuilder() { + return IfNullOperatorBuilder.newBuilder(); + } + + public static interface IfNullBuilder { + + /** + * @param field the field to check for a {@literal null} value, field reference must not be {@literal null}. + * @return the {@link ThenBuilder} + */ + ThenBuilder ifNull(Field field); + + /** + * @param field the field to check for a {@literal null} value, field name must not be {@literal null} or empty. + * @return the {@link ThenBuilder} + */ + ThenBuilder ifNull(String field); + } + + public static interface ThenBuilder { + + /** + * @param field the field holding the replacement value, must not be {@literal null}. + * @return the {@link IfNullOperator} + */ + IfNullOperator thenReplaceWith(Field field); + + /** + * @param value the value to be used if the {@code $ifNull }condition evaluates {@literal true}. Can be a + * {@link DBObject}, a value that is supported by MongoDB or a value that can be converted to a MongoDB + * representation but must not be {@literal null}. + * @return the {@link IfNullOperator} + */ + IfNullOperator thenReplaceWith(Object value); + } + + /** + * Builder for fluent {@link IfNullOperator} creation. + * + * @author Mark Paluch + * @since 1.10 + */ + public static final class IfNullOperatorBuilder implements IfNullBuilder, ThenBuilder { + + private Field field; + + private IfNullOperatorBuilder() {} + + /** + * Creates a new builder for {@link IfNullOperator}. + * + * @return never {@literal null}. + */ + public static IfNullOperatorBuilder newBuilder() { + return new IfNullOperatorBuilder(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.IfNullBuilder#ifNull(org.springframework.data.mongodb.core.aggregation.Field) + */ + + public ThenBuilder ifNull(Field field) { + + Assert.notNull(field, "Field must not be null!"); + + this.field = field; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.IfNullBuilder#ifNull(java.lang.String) + */ + public ThenBuilder ifNull(String name) { + + Assert.hasText(name, "Field name must not be null or empty!"); + + this.field = Fields.field(name); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.ThenReplaceBuilder#thenReplaceWith(org.springframework.data.mongodb.core.aggregation.Field) + */ + @Override + public IfNullOperator thenReplaceWith(Field replacementField) { + + Assert.notNull(replacementField, "Replacement field must not be null!"); + + return new IfNullOperator(this.field, replacementField); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.ThenReplaceBuilder#thenReplaceWith(java.lang.Object) + */ + @Override + public IfNullOperator thenReplaceWith(Object value) { + + Assert.notNull(value, "'Replacement-value' must not be null!"); + + return new IfNullOperator(this.field, value); + } + } +} 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 94fc9cb97a..574ecb54c0 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 @@ -104,8 +104,8 @@ private ProjectionOperation and(Projection projection) { */ private ProjectionOperation andReplaceLastOneWith(Projection projection) { - List projections = this.projections.isEmpty() ? Collections. emptyList() : this.projections - .subList(0, this.projections.size() - 1); + List projections = this.projections.isEmpty() ? Collections. emptyList() + : this.projections.subList(0, this.projections.size() - 1); return new ProjectionOperation(projections, Arrays.asList(projection)); } @@ -240,6 +240,24 @@ public DBObject toDBObject(AggregationOperationContext context) { * @return */ public abstract ProjectionOperation as(String alias); + + /** + * Apply a conditional projection using {@link ConditionalOperator}. + * + * @param conditional must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 + */ + public abstract ProjectionOperation transform(ConditionalOperator conditional); + + /** + * Apply a conditional value replacement for {@literal null} values using {@link IfNullOperator}. + * + * @param ifNull must not be {@literal null}. + * @return never {@literal null}. + * @since 1.10 + */ + public abstract ProjectionOperation transform(IfNullOperator ifNull); } /** @@ -370,7 +388,8 @@ public static class ProjectionOperationBuilder extends AbstractProjectionOperati * @param operation must not be {@literal null}. * @param previousProjection the previous operation projection, may be {@literal null}. */ - public ProjectionOperationBuilder(String name, ProjectionOperation operation, OperationProjection previousProjection) { + public ProjectionOperationBuilder(String name, ProjectionOperation operation, + OperationProjection previousProjection) { super(name, operation); this.name = name; @@ -419,7 +438,7 @@ public ProjectionOperation nested(Fields fields) { /** * Allows to specify an alias for the previous projection operation. * - * @param string + * @param alias * @return */ @Override @@ -436,6 +455,24 @@ public ProjectionOperation as(String alias) { return this.operation.and(new FieldProjection(Fields.field(alias, name), null)); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.AbstractProjectionOperationBuilder#transform(org.springframework.data.mongodb.core.aggregation.ConditionalOperator) + */ + @Override + public ProjectionOperation transform(ConditionalOperator conditional) { + return this.operation.and(new ExpressionProjection(Fields.field(name), conditional)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.AbstractProjectionOperationBuilder#transform(org.springframework.data.mongodb.core.aggregation.IfNullOperator) + */ + @Override + public ProjectionOperation transform(IfNullOperator ifNull) { + return this.operation.and(new ExpressionProjection(Fields.field(name), ifNull)); + } + /** * Generates an {@code $add} expression that adds the given number to the previously mentioned field. * @@ -764,7 +801,7 @@ public OperationProjection(Field field, String operation, Object[] values) { this.values = Arrays.asList(values); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.Projection#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) */ 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 b9ec9d853f..871a7cd8ae 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 @@ -20,6 +20,7 @@ import static org.junit.Assume.*; import static org.springframework.data.domain.Sort.Direction.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; +import static org.springframework.data.mongodb.core.aggregation.Fields.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*; @@ -55,6 +56,7 @@ import org.springframework.data.mongodb.core.Venue; import org.springframework.data.mongodb.core.aggregation.AggregationTests.CarDescriptor.Entry; import org.springframework.data.mongodb.core.index.GeospatialIndex; +import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; @@ -131,6 +133,8 @@ private void cleanDb() { mongoTemplate.dropCollection(Reservation.class); mongoTemplate.dropCollection(Venue.class); mongoTemplate.dropCollection(MeterData.class); + mongoTemplate.dropCollection(LineItem.class); + mongoTemplate.dropCollection(InventoryItem.class); } /** @@ -484,6 +488,250 @@ public void findStatesWithPopulationOver10MillionAggregationExample() { assertThat(stateStats.totalPopulation, is(29760021)); } + /** + * @see DATAMONGO-861 + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/cond/#example + */ + @Test + public void aggregationUsingConditionalProjectionToCalculateDiscount() { + + /* + db.inventory.aggregate( + [ + { + $project: + { + item: 1, + discount: + { + $cond: { if: { $gte: [ "$qty", 250 ] }, then: 30, else: 20 } + } + } + } + ] + ) + */ + + mongoTemplate.insert(new InventoryItem(1, "abc1", 300)); + mongoTemplate.insert(new InventoryItem(2, "abc2", 200)); + mongoTemplate.insert(new InventoryItem(3, "xyz1", 250)); + + TypedAggregation aggregation = newAggregation(InventoryItem.class, // + project("item") // + .and("discount")// + .transform(ConditionalOperator.newBuilder().when(Criteria.where("qty").gte(250)) // + .then(30) // + .otherwise(20))); + + assertThat(aggregation.toString(), is(notNullValue())); + + AggregationResults result = mongoTemplate.aggregate(aggregation, DBObject.class); + assertThat(result.getMappedResults().size(), is(3)); + + DBObject first = result.getMappedResults().get(0); + assertThat(first.get("_id"), is((Object) 1)); + assertThat(first.get("discount"), is((Object) 30)); + + DBObject second = result.getMappedResults().get(1); + assertThat(second.get("_id"), is((Object) 2)); + assertThat(second.get("discount"), is((Object) 20)); + + DBObject third = result.getMappedResults().get(2); + assertThat(third.get("_id"), is((Object) 3)); + assertThat(third.get("discount"), is((Object) 30)); + } + + /** + * @see DATAMONGO-861 + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/#example + */ + @Test + public void aggregationUsingIfNullToProjectSaneDefaults() { + + /* + db.inventory.aggregate( + [ + { + $project: { + item: 1, + description: { $ifNull: [ "$description", "Unspecified" ] } + } + } + ] + ) + */ + + mongoTemplate.insert(new InventoryItem(1, "abc1", "product 1", 300)); + mongoTemplate.insert(new InventoryItem(2, "abc2", 200)); + mongoTemplate.insert(new InventoryItem(3, "xyz1", 250)); + + TypedAggregation aggregation = newAggregation(InventoryItem.class, // + project("item") // + .and(ifNull("description", "Unspecified")) // + .as("description")// + ); + + assertThat(aggregation.toString(), is(notNullValue())); + + AggregationResults result = mongoTemplate.aggregate(aggregation, DBObject.class); + assertThat(result.getMappedResults().size(), is(3)); + + DBObject first = result.getMappedResults().get(0); + assertThat(first.get("_id"), is((Object) 1)); + assertThat(first.get("description"), is((Object) "product 1")); + + DBObject second = result.getMappedResults().get(1); + assertThat(second.get("_id"), is((Object) 2)); + assertThat(second.get("description"), is((Object) "Unspecified")); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void aggregationUsingConditionalProjection() { + + TypedAggregation aggregation = newAggregation(ZipInfo.class, // + project() // + .and("largePopulation")// + .transform(ConditionalOperator.newBuilder().when(Criteria.where("population").gte(20000)) // + .then(true) // + .otherwise(false)) // + .and("population").as("population")); + + assertThat(aggregation, is(notNullValue())); + assertThat(aggregation.toString(), is(notNullValue())); + + AggregationResults result = mongoTemplate.aggregate(aggregation, DBObject.class); + assertThat(result.getMappedResults().size(), is(29467)); + + DBObject firstZipInfoStats = result.getMappedResults().get(0); + assertThat(firstZipInfoStats.get("largePopulation"), is((Object) false)); + assertThat(firstZipInfoStats.get("population"), is((Object) 6055)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void aggregationUsingNestedConditionalProjection() { + + TypedAggregation aggregation = newAggregation(ZipInfo.class, // + project() // + .and("size")// + .transform(ConditionalOperator.newBuilder().when(Criteria.where("population").gte(20000)) // + .then(ConditionalOperator.newBuilder().when(Criteria.where("population").gte(200000)).then("huge") + .otherwise("small")) // + .otherwise("small")) // + .and("population").as("population")); + + assertThat(aggregation, is(notNullValue())); + assertThat(aggregation.toString(), is(notNullValue())); + + AggregationResults result = mongoTemplate.aggregate(aggregation, DBObject.class); + assertThat(result.getMappedResults().size(), is(29467)); + + DBObject firstZipInfoStats = result.getMappedResults().get(0); + assertThat(firstZipInfoStats.get("size"), is((Object) "small")); + assertThat(firstZipInfoStats.get("population"), is((Object) 6055)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void aggregationUsingIfNullProjection() { + + mongoTemplate.insert(new LineItem("id", "caption", 0)); + mongoTemplate.insert(new LineItem("idonly", null, 0)); + + TypedAggregation aggregation = newAggregation(LineItem.class, // + project("id") // + .and("caption")// + .transform(ifNull(field("caption"), "unknown")), + sort(ASC, "id")); + + assertThat(aggregation.toString(), is(notNullValue())); + + AggregationResults result = mongoTemplate.aggregate(aggregation, DBObject.class); + assertThat(result.getMappedResults().size(), is(2)); + + DBObject id = result.getMappedResults().get(0); + assertThat((String) id.get("caption"), is(equalTo("caption"))); + + DBObject idonly = result.getMappedResults().get(1); + assertThat((String) idonly.get("caption"), is(equalTo("unknown"))); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void aggregationUsingIfNullReplaceWithFieldReferenceProjection() { + + mongoTemplate.insert(new LineItem("id", "caption", 0)); + mongoTemplate.insert(new LineItem("idonly", null, 0)); + + TypedAggregation aggregation = newAggregation(LineItem.class, // + project("id") // + .and("caption")// + .transform(ifNull(field("caption"), field("id"))), + sort(ASC, "id")); + + assertThat(aggregation.toString(), is(notNullValue())); + + AggregationResults result = mongoTemplate.aggregate(aggregation, DBObject.class); + assertThat(result.getMappedResults().size(), is(2)); + + DBObject id = result.getMappedResults().get(0); + assertThat((String) id.get("caption"), is(equalTo("caption"))); + + DBObject idonly = result.getMappedResults().get(1); + assertThat((String) idonly.get("caption"), is(equalTo("idonly"))); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void shouldAllowGroupingUsingConditionalExpressions() { + + mongoTemplate.dropCollection(CarPerson.class); + + CarPerson person1 = new CarPerson("first1", "last1", new CarDescriptor.Entry("MAKE1", "MODEL1", 2000), + new CarDescriptor.Entry("MAKE1", "MODEL2", 2001)); + + CarPerson person2 = new CarPerson("first2", "last2", new CarDescriptor.Entry("MAKE3", "MODEL4", 2014)); + CarPerson person3 = new CarPerson("first3", "last3", new CarDescriptor.Entry("MAKE2", "MODEL5", 2015)); + + mongoTemplate.save(person1); + mongoTemplate.save(person2); + mongoTemplate.save(person3); + + TypedAggregation agg = Aggregation.newAggregation(CarPerson.class, + unwind("descriptors.carDescriptor.entries"), // + project() // + .and(new ConditionalOperator(Criteria.where("descriptors.carDescriptor.entries.make").is("MAKE1"), "good", + "meh")) + .as("make") // + .and("descriptors.carDescriptor.entries.model").as("model") // + .and("descriptors.carDescriptor.entries.year").as("year"), // + group("make").avg(new ConditionalOperator(Criteria.where("year").gte(2012), 1, 9000)).as("score"), + sort(ASC, "make")); + + AggregationResults result = mongoTemplate.aggregate(agg, DBObject.class); + + assertThat(result.getMappedResults(), hasSize(2)); + + DBObject meh = result.getMappedResults().get(0); + assertThat((String) meh.get("_id"), is(equalTo("meh"))); + assertThat(((Number) meh.get("score")).longValue(), is(equalTo(1L))); + + DBObject good = result.getMappedResults().get(1); + assertThat((String) good.get("_id"), is(equalTo("good"))); + assertThat(((Number) good.get("score")).longValue(), is(equalTo(9000L))); + } + /** * @see http://docs.mongodb.org/manual/tutorial/aggregation-examples/#return-the-five-most-common-likes */ @@ -1460,4 +1708,31 @@ public ObjectWithDate(Date dateValue) { this.dateValue = dateValue; } } + + /** + * @see DATAMONGO-861 + */ + @Document(collection = "inventory") + static class InventoryItem { + + int id; + String item; + String description; + int qty; + + public InventoryItem() {} + + public InventoryItem(int id, String item, int qty) { + this.id = id; + this.item = item; + this.qty = qty; + } + + public InventoryItem(int id, String item, String description, int qty) { + this.id = id; + this.item = item; + this.description = description; + this.qty = qty; + } + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java index 03387417c2..645e0a66b5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java @@ -19,6 +19,7 @@ import static org.junit.Assert.*; import static org.springframework.data.mongodb.core.DBObjectTestUtils.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; +import static org.springframework.data.mongodb.core.aggregation.Fields.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*; @@ -29,7 +30,10 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.test.util.BasicDbListBuilder; +import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObjectBuilder; import com.mongodb.DBObject; @@ -243,6 +247,25 @@ public void expressionBasedFieldsShouldBeReferencableInFollowingOperations() { assertThat(fields.get("foosum"), is((Object) new BasicDBObject("$sum", "$foo"))); } + /** + * @see DATAMONGO-861 + */ + @Test + public void conditionExpressionBasedFieldsShouldBeReferencableInFollowingOperations() { + + DBObject agg = newAggregation( // + project("a"), // + group("a").first(conditional(Criteria.where("a").gte(42), "answer", "no-answer")).as("foosum") // + ).toDbObject("foo", Aggregation.DEFAULT_CONTEXT); + + @SuppressWarnings("unchecked") + DBObject secondProjection = ((List) agg.get("pipeline")).get(1); + DBObject fields = getAsDBObject(secondProjection, "$group"); + assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first")); + assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first.$cond.then", "answer")); + assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first.$cond.else", "no-answer")); + } + /** * @see DATAMONGO-908 */ @@ -379,6 +402,160 @@ public void shouldUseAliasedFieldnameForProjectionsIncludingOperationsDownThePip assertThat(getAsDBObject(group, "count"), is(new BasicDBObjectBuilder().add("$sum", "$tags_count").get())); } + /** + * @see DATAMONGO-861 + */ + @Test + public void shouldRenderProjectionConditionalExpressionCorrectly() { + + DBObject agg = Aggregation.newAggregation(// + project().and(ConditionalOperator.newBuilder() // + .when("isYellow") // + .then("bright") // + .otherwise("dark")).as("color")) + .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); + + DBObject project = extractPipelineElement(agg, 0, "$project"); + DBObject expectedCondition = new BasicDBObject() // + .append("if", "$isYellow") // + .append("then", "bright") // + .append("else", "dark"); + + assertThat(getAsDBObject(project, "color"), isBsonObject().containing("$cond", expectedCondition)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void shouldRenderProjectionConditionalCorrectly() { + + DBObject agg = Aggregation.newAggregation(// + project().and("color") + .transform(ConditionalOperator.newBuilder() // + .when("isYellow") // + .then("bright") // + .otherwise("dark"))) + .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); + + DBObject project = extractPipelineElement(agg, 0, "$project"); + DBObject expectedCondition = new BasicDBObject() // + .append("if", "$isYellow") // + .append("then", "bright") // + .append("else", "dark"); + + assertThat(getAsDBObject(project, "color"), isBsonObject().containing("$cond", expectedCondition)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void shouldRenderProjectionConditionalWithCriteriaCorrectly() { + + DBObject agg = Aggregation + .newAggregation(project()// + .and("color")// + .transform(conditional(Criteria.where("key").gt(5), "bright", "dark"))) // + .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); + + DBObject project = extractPipelineElement(agg, 0, "$project"); + DBObject expectedCondition = new BasicDBObject() // + .append("if", new BasicDBObject("$gt", new BasicDbListBuilder().add("$key").add(5).get())) // + .append("then", "bright") // + .append("else", "dark"); + + assertThat(getAsDBObject(project, "color"), isBsonObject().containing("$cond", expectedCondition)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void referencingProjectionAliasesShouldRenderProjectionConditionalWithFieldReferenceCorrectly() { + + DBObject agg = Aggregation + .newAggregation(// + project().and("color").as("chroma"), + project().and("luminosity") // + .transform(conditional(field("chroma"), "bright", "dark"))) // + .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); + + DBObject project = extractPipelineElement(agg, 1, "$project"); + DBObject expectedCondition = new BasicDBObject() // + .append("if", "$chroma") // + .append("then", "bright") // + .append("else", "dark"); + + assertThat(getAsDBObject(project, "luminosity"), isBsonObject().containing("$cond", expectedCondition)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void referencingProjectionAliasesShouldRenderProjectionConditionalWithCriteriaReferenceCorrectly() { + + DBObject agg = Aggregation + .newAggregation(// + project().and("color").as("chroma"), + project().and("luminosity") // + .transform(conditional(Criteria.where("chroma").is(100), "bright", "dark"))) // + .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); + + DBObject project = extractPipelineElement(agg, 1, "$project"); + DBObject expectedCondition = new BasicDBObject() // + .append("if", new BasicDBObject("$eq", new BasicDbListBuilder().add("$chroma").add(100).get())) // + .append("then", "bright") // + .append("else", "dark"); + + assertThat(getAsDBObject(project, "luminosity"), isBsonObject().containing("$cond", expectedCondition)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void shouldRenderProjectionIfNullWithFieldReferenceCorrectly() { + + DBObject agg = Aggregation + .newAggregation(// + project().and("color"), // + project().and("luminosity") // + .transform(ifNull(field("chroma"), "unknown"))) // + .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); + + DBObject project = extractPipelineElement(agg, 1, "$project"); + BasicDBList expectedCondition = new BasicDbListBuilder() // + .add("$chroma") // + .add("unknown")// + .get(); + + assertThat(getAsDBObject(project, "luminosity"), isBsonObject().containing("$ifNull", expectedCondition)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void shouldRenderProjectionIfNullWithFallbackFieldReferenceCorrectly() { + + DBObject agg = Aggregation + .newAggregation(// + project("fallback").and("color").as("chroma"), + project().and("luminosity") // + .transform(ifNull(field("chroma"), field("fallback")))) // + .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); + + DBObject project = extractPipelineElement(agg, 1, "$project"); + BasicDBList expectedCondition = new BasicDbListBuilder() // + .add("$chroma") // + .add("$fallback")// + .get(); + + assertThat(getAsDBObject(project, "luminosity"), isBsonObject().containing("$ifNull", expectedCondition)); + } + private DBObject extractPipelineElement(DBObject agg, int index, String operation) { List pipeline = (List) agg.get("pipeline"); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperatorUnitTests.java new file mode 100644 index 0000000000..8eaf380b4e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperatorUnitTests.java @@ -0,0 +1,210 @@ +/* + * 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.junit.Assert.*; +import static org.springframework.data.mongodb.core.aggregation.ConditionalOperator.*; +import static org.springframework.data.mongodb.test.util.IsBsonObject.*; + +import org.junit.Test; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.test.util.BasicDbListBuilder; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Unit tests for {@link ConditionalOperator}. + * + * @author Mark Paluch + */ +public class ConditionalOperatorUnitTests { + + /** + * @see DATAMONGO-861 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldRejectNullCondition() { + new ConditionalOperator((Field) null, "", ""); + } + + /** + * @see DATAMONGO-861 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldRejectThenValue() { + new ConditionalOperator(Fields.field("field"), null, ""); + } + + /** + * @see DATAMONGO-861 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldRejectOtherwiseValue() { + new ConditionalOperator(Fields.field("field"), "", null); + } + + /** + * @see DATAMONGO-861 + */ + @Test(expected = IllegalArgumentException.class) + public void builderRejectsEmptyFieldName() { + newBuilder().when(""); + } + + /** + * @see DATAMONGO-861 + */ + @Test(expected = IllegalArgumentException.class) + public void builderRejectsNullFieldName() { + newBuilder().when((DBObject) null); + } + + /** + * @see DATAMONGO-861 + */ + @Test(expected = IllegalArgumentException.class) + public void builderRejectsNullCriteriaName() { + newBuilder().when((Criteria) null); + } + + /** + * @see DATAMONGO-861 + */ + @Test(expected = IllegalArgumentException.class) + public void builderRejectsBuilderAsThenValue() { + newBuilder().when("isYellow").then(newBuilder().when("field").then("then-value")).otherwise("otherwise"); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void simpleBuilderShouldRenderCorrectly() { + + ConditionalOperator operator = newBuilder().when("isYellow").then("bright").otherwise("dark"); + DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); + + DBObject expectedCondition = new BasicDBObject() // + .append("if", "$isYellow") // + .append("then", "bright") // + .append("else", "dark"); + + assertThat(dbObject, isBsonObject().containing("$cond", expectedCondition)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void simpleCriteriaShouldRenderCorrectly() { + + ConditionalOperator operator = newBuilder().when(Criteria.where("luminosity").gte(100)).then("bright") + .otherwise("dark"); + DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); + + DBObject expectedCondition = new BasicDBObject() // + .append("if", new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(100).get())) // + .append("then", "bright") // + .append("else", "dark"); + + assertThat(dbObject, isBsonObject().containing("$cond", expectedCondition)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void andCriteriaShouldRenderCorrectly() { + + ConditionalOperator operator = newBuilder() // + .when(Criteria.where("luminosity").gte(100) // + .andOperator(Criteria.where("hue").is(50), // + Criteria.where("saturation").lt(11))) + .then("bright").otherwise("dark"); + + DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); + + BasicDBObject luminosity = new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(100).get()); + BasicDBObject hue = new BasicDBObject("$eq", new BasicDbListBuilder().add("$hue").add(50).get()); + BasicDBObject saturation = new BasicDBObject("$lt", new BasicDbListBuilder().add("$saturation").add(11).get()); + + DBObject expectedCondition = new BasicDBObject() // + .append("if", + new BasicDbListBuilder().add(luminosity) + .add(new BasicDBObject("$and", new BasicDbListBuilder().add(hue).add(saturation).get())).get()) // + .append("then", "bright") // + .append("else", "dark"); + + assertThat(dbObject, isBsonObject().containing("$cond", expectedCondition)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void twoArgsCriteriaShouldRenderCorrectly() { + + Criteria criteria = Criteria.where("luminosity").gte(100) // + .and("saturation").and("chroma").is(200); + ConditionalOperator operator = newBuilder().when(criteria).then("bright").otherwise("dark"); + + DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); + + BasicDBObject gte = new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(100).get()); + BasicDBObject is = new BasicDBObject("$eq", new BasicDbListBuilder().add("$chroma").add(200).get()); + + DBObject expectedCondition = new BasicDBObject() // + .append("if", new BasicDbListBuilder().add(gte).add(is).get()) // + .append("then", "bright") // + .append("else", "dark"); + + assertThat(dbObject, isBsonObject().containing("$cond", expectedCondition)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void nestedCriteriaShouldRenderCorrectly() { + + ConditionalOperator operator = newBuilder() // + .when(Criteria.where("luminosity").gte(100)) // + .then(newBuilder() // + .when(Criteria.where("luminosity").gte(200)) // + .then("verybright") // + .otherwise("not-so-bright")) // + .otherwise(newBuilder() // + .when(Criteria.where("luminosity").lt(50)) // + .then("very-dark") // + .otherwise("not-so-dark")); + + DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); + + DBObject trueCondition = new BasicDBObject() // + .append("if", new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(200).get())) // + .append("then", "verybright") // + .append("else", "not-so-bright"); + + DBObject falseCondition = new BasicDBObject() // + .append("if", new BasicDBObject("$lt", new BasicDbListBuilder().add("$luminosity").add(50).get())) // + .append("then", "very-dark") // + .append("else", "not-so-dark"); + + assertThat(dbObject, isBsonObject().containing("$cond.then.$cond", trueCondition)); + assertThat(dbObject, isBsonObject().containing("$cond.else.$cond", falseCondition)); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/IfNullOperatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/IfNullOperatorUnitTests.java new file mode 100644 index 0000000000..5ecf68f3a8 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/IfNullOperatorUnitTests.java @@ -0,0 +1,90 @@ +/* + * 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.junit.Assert.*; +import static org.springframework.data.mongodb.test.util.IsBsonObject.*; + +import org.junit.Test; +import org.springframework.data.mongodb.test.util.BasicDbListBuilder; + +import com.mongodb.BasicDBList; +import com.mongodb.DBObject; + +/** + * Unit tests for {@link IfNullOperator}. + * + * @author Mark Paluch + */ +public class IfNullOperatorUnitTests { + + /** + * @see DATAMONGO-861 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldRejectNullCondition() { + new IfNullOperator(null, ""); + } + + /** + * @see DATAMONGO-861 + */ + @Test(expected = IllegalArgumentException.class) + public void shouldRejectThenValue() { + new IfNullOperator(Fields.field("aa"), null); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void simpleIfNullShouldRenderCorrectly() { + + IfNullOperator operator = IfNullOperator.newBuilder() // + .ifNull("optional") // + .thenReplaceWith("a more sophisticated value"); + + DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); + + BasicDBList expected = new BasicDbListBuilder() // + .add("$optional") // + .add("a more sophisticated value")// + .get(); + + assertThat(dbObject, isBsonObject().containing("$ifNull", expected)); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void fieldReplacementIfNullShouldRenderCorrectly() { + + IfNullOperator operator = IfNullOperator.newBuilder() // + .ifNull(Fields.field("optional")) // + .thenReplaceWith(Fields.field("never-null")); + + DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); + + BasicDBList expected = new BasicDbListBuilder() // + .add("$optional") // + .add("$never-null")// + .get(); + + assertThat(dbObject, isBsonObject().containing("$ifNull", expected)); + } +} 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 eece18bca2..a0322a5880 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 @@ -18,6 +18,8 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; +import static org.springframework.data.mongodb.core.aggregation.Fields.*; +import static org.springframework.data.mongodb.test.util.IsBsonObject.*; import java.util.Arrays; import java.util.List; @@ -89,7 +91,7 @@ public void returnsReferencesToNestedFieldsCorrectly() { AggregationOperationContext context = getContext(Foo.class); - Field field = Fields.field("bar.name"); + Field field = field("bar.name"); assertThat(context.getReference("bar.name"), is(notNullValue())); assertThat(context.getReference(field), is(notNullValue())); @@ -103,7 +105,7 @@ public void returnsReferencesToNestedFieldsCorrectly() { public void aliasesIdFieldCorrectly() { AggregationOperationContext context = getContext(Foo.class); - assertThat(context.getReference("id"), is(new FieldReference(new ExposedField(Fields.field("id", "_id"), true)))); + assertThat(context.getReference("id"), is(new FieldReference(new ExposedField(field("id", "_id"), true)))); } /** @@ -279,6 +281,58 @@ public void lookupGroupAggregationShouldFailInvalidFieldReference() { agg.toDbObject("meterData", context); } + /** + * @see DATAMONGO-861 + */ + @Test + public void rendersAggregationConditionalInTypedAggregationContextCorrectly() { + + AggregationOperationContext context = getContext(FooPerson.class); + TypedAggregation agg = newAggregation(FooPerson.class, + project("name") // + .and("age") // + .transform(conditional(Criteria.where("age.value").lt(10), new Age(0), field("age"))) // + ); + + DBObject dbo = agg.toDbObject("person", context); + + DBObject projection = getPipelineElementFromAggregationAt(dbo, 0); + assertThat(projection.containsField("$project"), is(true)); + + DBObject project = getValue(projection, "$project"); + DBObject age = getValue(project, "age"); + + assertThat((DBObject) getValue(age, "$cond"), isBsonObject().containing("then.value", 0)); + assertThat((DBObject) getValue(age, "$cond"), isBsonObject().containing("then._class", Age.class.getName())); + assertThat((DBObject) getValue(age, "$cond"), isBsonObject().containing("else", "$age")); + } + + /** + * @see DATAMONGO-861 + */ + @Test + public void rendersAggregationIfNullInTypedAggregationContextCorrectly() { + + AggregationOperationContext context = getContext(FooPerson.class); + TypedAggregation agg = newAggregation(FooPerson.class, + project("name") // + .and("age") // + .transform(ifNull("age", new Age(0))) // + ); + + DBObject dbo = agg.toDbObject("person", context); + + DBObject projection = getPipelineElementFromAggregationAt(dbo, 0); + assertThat(projection.containsField("$project"), is(true)); + + DBObject project = getValue(projection, "$project"); + DBObject age = getValue(project, "age"); + + assertThat(age, isBsonObject().containing("$ifNull.[0]", "$age")); + assertThat(age, isBsonObject().containing("$ifNull.[1].value", 0)); + assertThat(age, isBsonObject().containing("$ifNull.[1]._class", Age.class.getName())); + } + @Document(collection = "person") public static class FooPerson { diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 032b8df74b..8066c6a086 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -4,6 +4,7 @@ [[new-features.1-10-0]] == What's new in Spring Data MongoDB 1.10 * Support for `$min`, `$max` and `$slice` operators via `Update`. +* Support for `$cond` and `$ifNull` operators via `Aggregation`. [[new-features.1-9-0]] == What's new in Spring Data MongoDB 1.9 diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 42a4075bf4..3d396a4dbd 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1688,6 +1688,9 @@ At the time of this writing we provide support for the following Aggregation Ope | Array Aggregation Operators | size, slice +| Conditional Aggregation Operators +| cond, ifNull + |=== Note that the aggregation operations not listed here are currently not supported by Spring Data MongoDB. Comparison aggregation operators are expressed as `Criteria` expressions. @@ -1985,6 +1988,50 @@ List resultList = result.getMappedResults(); Note that we can also refer to other fields of the document within the SpEL expression. +[[mongo.aggregation.examples.example6]] +.Aggregation Framework Example 6 + +This example uses conditional projection. It's derived from the https://docs.mongodb.com/manual/reference/operator/aggregation/cond/[$cond reference documentation]. + +[source,java] +---- +public class InventoryItem { + + @Id int id; + String item; + String description; + int qty; +} + +public class InventoryItemProjection { + + @Id int id; + String item; + String description; + int qty; + int discount +} +---- + +[source,java] +---- +import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; + +TypedAggregation agg = newAggregation(InventoryItem.class, + project("item").and("discount") + .transform(ConditionalOperator.newBuilder().when(Criteria.where("qty").gte(250)) + .then(30) + .otherwise(20)) + .and(ifNull("description", "Unspecified")).as("description") +); + +AggregationResults result = mongoTemplate.aggregate(agg, "inventory", InventoryItemProjection.class); +List stateStatsList = result.getMappedResults(); +---- + +* This one-step aggregation uses a projection operation with the `inventory` collection. We project the `discount` field using a conditional transformation for all inventory items that have a `qty` greater or equal to `250`. An second conditional projection is performed for the `description` field. We apply the description `Unspecified` to all items that either do not have a `description` field of items that have a `null` description. + + [[mongo.custom-converters]] == Overriding default mapping with custom converters From 8d239c1eac4ddee51101251c7cfdc9096e74654c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 25 Aug 2016 14:44:36 +0200 Subject: [PATCH 3/3] DATAMONGO-861 - Polishing. Favor usage of List over BasicDBList. Rename ProjectionOperation.transform to applyCondition. Add missing author and since tags, remove trailing white spaces and fix reference documentation headline clash. --- .../mongodb/core/aggregation/Aggregation.java | 12 +-- .../core/aggregation/ConditionalOperator.java | 53 +++++++---- .../core/aggregation/IfNullOperator.java | 43 +++++---- .../core/aggregation/ProjectionOperation.java | 28 +++--- .../core/aggregation/AggregationTests.java | 65 +++++++------ .../aggregation/AggregationUnitTests.java | 94 +++++++++---------- .../ConditionalOperatorUnitTests.java | 26 ++--- .../aggregation/IfNullOperatorUnitTests.java | 20 ++-- ...dAggregationOperationContextUnitTests.java | 4 +- .../data/mongodb/test/util/IsBsonObject.java | 10 +- src/main/asciidoc/reference/mongodb.adoc | 8 +- 11 files changed, 196 insertions(+), 167 deletions(-) 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 375e7e72a7..ea831b6119 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,7 +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.Fields.*; +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; import org.springframework.data.mongodb.core.query.NearQuery; @@ -331,13 +331,13 @@ public static MatchOperation match(Criteria criteria) { } /** - * Creates a new {@link OutOperation} using the given collection name. This operation must be the last operation - * in the pipeline. + * Creates a new {@link OutOperation} using the given collection name. This operation must be the last operation in + * the pipeline. * * @param outCollectionName collection name to export aggregation results. The {@link OutOperation} creates a new - * collection in the current database if one does not already exist. The collection is - * not visible until the aggregation completes. If the aggregation fails, MongoDB does - * not create the collection. Must not be {@literal null}. + * collection in the current database if one does not already exist. The collection is not visible until the + * aggregation completes. If the aggregation fails, MongoDB does not create the collection. Must not be + * {@literal null}. * @return */ public static OutOperation out(String outCollectionName) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperator.java index 3a0d5575d9..881894a14f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperator.java @@ -15,12 +15,14 @@ */ package org.springframework.data.mongodb.core.aggregation; +import java.util.ArrayList; +import java.util.List; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; @@ -32,6 +34,7 @@ * * @see http://docs.mongodb.com/manual/reference/operator/aggregation/cond/ * @author Mark Paluch + * @author Christoph Strobl * @since 1.10 */ public class ConditionalOperator implements AggregationExpression { @@ -42,7 +45,7 @@ public class ConditionalOperator implements AggregationExpression { /** * Creates a new {@link ConditionalOperator} for a given {@link Field} and {@code then}/{@code otherwise} values. - * + * * @param condition must not be {@literal null}. * @param thenValue must not be {@literal null}. * @param otherwiseValue must not be {@literal null}. @@ -116,8 +119,7 @@ private Object resolveValue(AggregationOperationContext context, Object value) { return ((ConditionalOperator) value).toDbObject(context); } - DBObject toMap = context.getMappedObject(new BasicDBObject("$set", value)); - return toMap.get("$set"); + return context.getMappedObject(new BasicDBObject("$set", value)).get("$set"); } private Object resolveCriteria(AggregationOperationContext context, Object value) { @@ -129,7 +131,7 @@ private Object resolveCriteria(AggregationOperationContext context, Object value if (value instanceof CriteriaDefinition) { DBObject mappedObject = context.getMappedObject(((CriteriaDefinition) value).getCriteriaObject()); - BasicDBList clauses = new BasicDBList(); + List clauses = new ArrayList(); clauses.addAll(getClauses(context, mappedObject)); @@ -144,9 +146,9 @@ private Object resolveCriteria(AggregationOperationContext context, Object value String.format("Invalid value in condition. Supported: DBObject, Field references, Criteria, got: %s", value)); } - private BasicDBList getClauses(AggregationOperationContext context, DBObject mappedObject) { + private List getClauses(AggregationOperationContext context, DBObject mappedObject) { - BasicDBList clauses = new BasicDBList(); + List clauses = new ArrayList(); for (String key : mappedObject.keySet()) { @@ -157,15 +159,17 @@ private BasicDBList getClauses(AggregationOperationContext context, DBObject map return clauses; } - private BasicDBList getClauses(AggregationOperationContext context, String key, Object predicate) { + private List getClauses(AggregationOperationContext context, String key, Object predicate) { - BasicDBList clauses = new BasicDBList(); + List clauses = new ArrayList(); - if (predicate instanceof BasicDBList) { + if (predicate instanceof List) { - BasicDBList args = new BasicDBList(); - for (Object clause : (BasicDBList) predicate) { - args.addAll(getClauses(context, (BasicDBObject) clause)); + List args = new ArrayList(); + for (Object clause : (List) predicate) { + if (clause instanceof DBObject) { + args.addAll(getClauses(context, (DBObject) clause)); + } } clauses.add(new BasicDBObject(key, args)); @@ -180,7 +184,7 @@ private BasicDBList getClauses(AggregationOperationContext context, String key, continue; } - BasicDBList args = new BasicDBList(); + List args = new ArrayList(); args.add("$" + key); args.add(nested.get(s)); clauses.add(new BasicDBObject(s, args)); @@ -188,7 +192,7 @@ private BasicDBList getClauses(AggregationOperationContext context, String key, } else if (!isKeyword(key)) { - BasicDBList args = new BasicDBList(); + List args = new ArrayList(); args.add("$" + key); args.add(predicate); clauses.add(new BasicDBObject("$eq", args)); @@ -230,6 +234,9 @@ public static ConditionalExpressionBuilder newBuilder() { return ConditionalExpressionBuilder.newBuilder(); } + /** + * @since 1.10 + */ public static interface WhenBuilder { /** @@ -257,6 +264,9 @@ public static interface WhenBuilder { ThenBuilder when(CriteriaDefinition criteria); } + /** + * @since 1.10 + */ public static interface ThenBuilder { /** @@ -268,6 +278,9 @@ public static interface ThenBuilder { OtherwiseBuilder then(Object value); } + /** + * @since 1.10 + */ public static interface OtherwiseBuilder { /** @@ -314,7 +327,7 @@ public ConditionalExpressionBuilder when(DBObject booleanExpression) { return this; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.WhenBuilder#when(org.springframework.data.mongodb.core.query.CriteriaDefinition) */ @@ -327,7 +340,7 @@ public ThenBuilder when(CriteriaDefinition criteria) { return this; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.WhenBuilder#when(org.springframework.data.mongodb.core.aggregation.Field) */ @@ -340,7 +353,7 @@ public ThenBuilder when(Field booleanField) { return this; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.WhenBuilder#when(java.lang.String) */ @@ -353,7 +366,7 @@ public ThenBuilder when(String booleanField) { return this; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.ThenBuilder#then(java.lang.Object) */ @@ -366,7 +379,7 @@ public OtherwiseBuilder then(Object thenValue) { return this; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.ConditionalOperator.OtherwiseBuilder#otherwise(java.lang.Object) */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/IfNullOperator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/IfNullOperator.java index bf3a59af23..b51aba01bb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/IfNullOperator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/IfNullOperator.java @@ -16,16 +16,18 @@ package org.springframework.data.mongodb.core.aggregation; +import java.util.ArrayList; +import java.util.List; + import org.springframework.util.Assert; -import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; /** * Encapsulates the aggregation framework {@code $ifNull} operator. Replacement values can be either {@link Field field * references}, values of simple MongoDB types or values that can be converted to a simple MongoDB type. - * + * * @see http://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/ * @author Mark Paluch * @since 1.10 @@ -37,7 +39,7 @@ public class IfNullOperator implements AggregationExpression { /** * Creates a new {@link IfNullOperator} for the given {@link Field} and replacement {@code value}. - * + * * @param field must not be {@literal null}. * @param value must not be {@literal null}. */ @@ -50,26 +52,30 @@ public IfNullOperator(Field field, Object value) { this.value = value; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.AggregationExpression#toDbObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) */ @Override public DBObject toDbObject(AggregationOperationContext context) { - BasicDBList list = new BasicDBList(); + List list = new ArrayList(); list.add(context.getReference(field).toString()); + list.add(resolve(value, context)); - if (value instanceof Field) { - list.add(context.getReference((Field) value).toString()); - } else { + return new BasicDBObject("$ifNull", list); + } - DBObject toMap = context.getMappedObject(new BasicDBObject("$set", value)); - list.add(toMap.get("$set")); + private Object resolve(Object value, AggregationOperationContext context) { + + if (value instanceof Field) { + return context.getReference((Field) value).toString(); + } else if (value instanceof DBObject) { + return value; } - return new BasicDBObject("$ifNull", list); + return context.getMappedObject(new BasicDBObject("$set", value)).get("$set"); } /** @@ -81,6 +87,9 @@ public static IfNullBuilder newBuilder() { return IfNullOperatorBuilder.newBuilder(); } + /** + * @since 1.10 + */ public static interface IfNullBuilder { /** @@ -96,6 +105,9 @@ public static interface IfNullBuilder { ThenBuilder ifNull(String field); } + /** + * @since 1.10 + */ public static interface ThenBuilder { /** @@ -134,11 +146,10 @@ public static IfNullOperatorBuilder newBuilder() { return new IfNullOperatorBuilder(); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.IfNullBuilder#ifNull(org.springframework.data.mongodb.core.aggregation.Field) */ - public ThenBuilder ifNull(Field field) { Assert.notNull(field, "Field must not be null!"); @@ -147,7 +158,7 @@ public ThenBuilder ifNull(Field field) { return this; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.IfNullBuilder#ifNull(java.lang.String) */ @@ -159,7 +170,7 @@ public ThenBuilder ifNull(String name) { return this; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.ThenReplaceBuilder#thenReplaceWith(org.springframework.data.mongodb.core.aggregation.Field) */ @@ -171,7 +182,7 @@ public IfNullOperator thenReplaceWith(Field replacementField) { return new IfNullOperator(this.field, replacementField); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.IfNullOperator.ThenReplaceBuilder#thenReplaceWith(java.lang.Object) */ 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 574ecb54c0..73e11bf4dd 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 @@ -42,6 +42,7 @@ * @author Thomas Darimont * @author Oliver Gierke * @author Christoph Strobl + * @author Mark Paluch * @since 1.3 */ public class ProjectionOperation implements FieldsExposingAggregationOperation { @@ -244,20 +245,20 @@ public DBObject toDBObject(AggregationOperationContext context) { /** * Apply a conditional projection using {@link ConditionalOperator}. * - * @param conditional must not be {@literal null}. + * @param conditionalOperator must not be {@literal null}. * @return never {@literal null}. * @since 1.10 */ - public abstract ProjectionOperation transform(ConditionalOperator conditional); + public abstract ProjectionOperation applyCondition(ConditionalOperator conditionalOperator); /** * Apply a conditional value replacement for {@literal null} values using {@link IfNullOperator}. * - * @param ifNull must not be {@literal null}. + * @param ifNullOperator must not be {@literal null}. * @return never {@literal null}. * @since 1.10 */ - public abstract ProjectionOperation transform(IfNullOperator ifNull); + public abstract ProjectionOperation applyCondition(IfNullOperator ifNullOperator); } /** @@ -359,7 +360,8 @@ public DBObject toDBObject(AggregationOperationContext context) { return new BasicDBObject(getExposedField().getName(), toMongoExpression(context, expression, params)); } - protected static Object toMongoExpression(AggregationOperationContext context, String expression, Object[] params) { + protected static Object toMongoExpression(AggregationOperationContext context, String expression, + Object[] params) { return TRANSFORMER.transform(expression, context, params); } } @@ -455,22 +457,26 @@ public ProjectionOperation as(String alias) { return this.operation.and(new FieldProjection(Fields.field(alias, name), null)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.AbstractProjectionOperationBuilder#transform(org.springframework.data.mongodb.core.aggregation.ConditionalOperator) */ @Override - public ProjectionOperation transform(ConditionalOperator conditional) { - return this.operation.and(new ExpressionProjection(Fields.field(name), conditional)); + public ProjectionOperation applyCondition(ConditionalOperator conditionalOperator) { + + Assert.notNull(conditionalOperator, "ConditionalOperator must not be null!"); + return this.operation.and(new ExpressionProjection(Fields.field(name), conditionalOperator)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.ProjectionOperation.AbstractProjectionOperationBuilder#transform(org.springframework.data.mongodb.core.aggregation.IfNullOperator) */ @Override - public ProjectionOperation transform(IfNullOperator ifNull) { - return this.operation.and(new ExpressionProjection(Fields.field(name), ifNull)); + public ProjectionOperation applyCondition(IfNullOperator ifNullOperator) { + + Assert.notNull(ifNullOperator, "IfNullOperator must not be null!"); + return this.operation.and(new ExpressionProjection(Fields.field(name), ifNullOperator)); } /** 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 871a7cd8ae..9acc010010 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 @@ -519,7 +519,7 @@ public void aggregationUsingConditionalProjectionToCalculateDiscount() { TypedAggregation aggregation = newAggregation(InventoryItem.class, // project("item") // .and("discount")// - .transform(ConditionalOperator.newBuilder().when(Criteria.where("qty").gte(250)) // + .applyCondition(ConditionalOperator.newBuilder().when(Criteria.where("qty").gte(250)) // .then(30) // .otherwise(20))); @@ -594,7 +594,7 @@ public void aggregationUsingConditionalProjection() { TypedAggregation aggregation = newAggregation(ZipInfo.class, // project() // .and("largePopulation")// - .transform(ConditionalOperator.newBuilder().when(Criteria.where("population").gte(20000)) // + .applyCondition(ConditionalOperator.newBuilder().when(Criteria.where("population").gte(20000)) // .then(true) // .otherwise(false)) // .and("population").as("population")); @@ -619,7 +619,7 @@ public void aggregationUsingNestedConditionalProjection() { TypedAggregation aggregation = newAggregation(ZipInfo.class, // project() // .and("size")// - .transform(ConditionalOperator.newBuilder().when(Criteria.where("population").gte(20000)) // + .applyCondition(ConditionalOperator.newBuilder().when(Criteria.where("population").gte(20000)) // .then(ConditionalOperator.newBuilder().when(Criteria.where("population").gte(200000)).then("huge") .otherwise("small")) // .otherwise("small")) // @@ -648,7 +648,7 @@ public void aggregationUsingIfNullProjection() { TypedAggregation aggregation = newAggregation(LineItem.class, // project("id") // .and("caption")// - .transform(ifNull(field("caption"), "unknown")), + .applyCondition(ifNull(field("caption"), "unknown")), sort(ASC, "id")); assertThat(aggregation.toString(), is(notNullValue())); @@ -675,7 +675,7 @@ public void aggregationUsingIfNullReplaceWithFieldReferenceProjection() { TypedAggregation aggregation = newAggregation(LineItem.class, // project("id") // .and("caption")// - .transform(ifNull(field("caption"), field("id"))), + .applyCondition(ifNull(field("caption"), field("id"))), sort(ASC, "id")); assertThat(aggregation.toString(), is(notNullValue())); @@ -798,11 +798,12 @@ public void arithmenticOperatorsInProjectionExample() { assertThat((Double) resultList.get(0).get("netPriceMul2"), is(product.netPrice * 2)); assertThat((Double) resultList.get(0).get("netPriceDiv119"), is(product.netPrice / 1.19)); assertThat((Integer) resultList.get(0).get("spaceUnitsMod2"), is(product.spaceUnits % 2)); - assertThat((Integer) resultList.get(0).get("spaceUnitsPlusSpaceUnits"), is(product.spaceUnits + product.spaceUnits)); + assertThat((Integer) resultList.get(0).get("spaceUnitsPlusSpaceUnits"), + is(product.spaceUnits + product.spaceUnits)); assertThat((Integer) resultList.get(0).get("spaceUnitsMinusSpaceUnits"), is(product.spaceUnits - product.spaceUnits)); - assertThat((Integer) resultList.get(0).get("spaceUnitsMultiplySpaceUnits"), is(product.spaceUnits - * product.spaceUnits)); + assertThat((Integer) resultList.get(0).get("spaceUnitsMultiplySpaceUnits"), + is(product.spaceUnits * product.spaceUnits)); assertThat((Double) resultList.get(0).get("spaceUnitsDivideSpaceUnits"), is((double) (product.spaceUnits / product.spaceUnits))); assertThat((Integer) resultList.get(0).get("spaceUnitsModSpaceUnits"), is(product.spaceUnits % product.spaceUnits)); @@ -891,8 +892,8 @@ public void expressionsInProjectionExampleShowcase() { DBObject firstItem = resultList.get(0); assertThat((String) firstItem.get("_id"), is(product.id)); assertThat((String) firstItem.get("name"), is(product.name)); - assertThat((Double) firstItem.get("salesPrice"), is((product.netPrice * (1 - product.discountRate) + shippingCosts) - * (1 + product.taxRate))); + assertThat((Double) firstItem.get("salesPrice"), + is((product.netPrice * (1 - product.discountRate) + shippingCosts) * (1 + product.taxRate))); } @Test @@ -914,7 +915,7 @@ public void shouldThrowExceptionIfUnknownFieldIsReferencedInArithmenticExpressio /** * @see DATAMONGO-753 - * @see http + * @see http * ://stackoverflow.com/questions/18653574/spring-data-mongodb-aggregation-framework-invalid-reference-in-group * -operati */ @@ -944,7 +945,7 @@ public void allowsNestedFieldReferencesAsGroupIdsInGroupExpressions() { /** * @see DATAMONGO-753 - * @see http + * @see http * ://stackoverflow.com/questions/18653574/spring-data-mongodb-aggregation-framework-invalid-reference-in-group * -operati */ @@ -979,12 +980,13 @@ public void shouldPerformDateProjectionOperatorsCorrectly() throws ParseExceptio data.stringValue = "ABC"; mongoTemplate.insert(data); - TypedAggregation agg = newAggregation(Data.class, project() // - .andExpression("concat(stringValue, 'DE')").as("concat") // - .andExpression("strcasecmp(stringValue,'XYZ')").as("strcasecmp") // - .andExpression("substr(stringValue,1,1)").as("substr") // - .andExpression("toLower(stringValue)").as("toLower") // - .andExpression("toUpper(toLower(stringValue))").as("toUpper") // + TypedAggregation agg = newAggregation(Data.class, + project() // + .andExpression("concat(stringValue, 'DE')").as("concat") // + .andExpression("strcasecmp(stringValue,'XYZ')").as("strcasecmp") // + .andExpression("substr(stringValue,1,1)").as("substr") // + .andExpression("toLower(stringValue)").as("toLower") // + .andExpression("toUpper(toLower(stringValue))").as("toUpper") // ); AggregationResults results = mongoTemplate.aggregate(agg, DBObject.class); @@ -1010,17 +1012,18 @@ public void shouldPerformStringProjectionOperatorsCorrectly() throws ParseExcept data.dateValue = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss.SSSZ").parse("29.08.1983 12:34:56.789+0000"); mongoTemplate.insert(data); - TypedAggregation agg = newAggregation(Data.class, project() // - .andExpression("dayOfYear(dateValue)").as("dayOfYear") // - .andExpression("dayOfMonth(dateValue)").as("dayOfMonth") // - .andExpression("dayOfWeek(dateValue)").as("dayOfWeek") // - .andExpression("year(dateValue)").as("year") // - .andExpression("month(dateValue)").as("month") // - .andExpression("week(dateValue)").as("week") // - .andExpression("hour(dateValue)").as("hour") // - .andExpression("minute(dateValue)").as("minute") // - .andExpression("second(dateValue)").as("second") // - .andExpression("millisecond(dateValue)").as("millisecond") // + TypedAggregation agg = newAggregation(Data.class, + project() // + .andExpression("dayOfYear(dateValue)").as("dayOfYear") // + .andExpression("dayOfMonth(dateValue)").as("dayOfMonth") // + .andExpression("dayOfWeek(dateValue)").as("dayOfWeek") // + .andExpression("year(dateValue)").as("year") // + .andExpression("month(dateValue)").as("month") // + .andExpression("week(dateValue)").as("week") // + .andExpression("hour(dateValue)").as("hour") // + .andExpression("minute(dateValue)").as("minute") // + .andExpression("second(dateValue)").as("second") // + .andExpression("millisecond(dateValue)").as("millisecond") // ); AggregationResults results = mongoTemplate.aggregate(agg, DBObject.class); @@ -1132,7 +1135,7 @@ public void shouldAggregateOrderDataToAnInvoice() { .and("orderId").previousOperation() // .andExpression("netAmount * [0]", taxRate).as("taxAmount") // .andExpression("netAmount * (1 + [0])", taxRate).as("totalAmount") // - ), Invoice.class); + ), Invoice.class); Invoice invoice = results.getUniqueMappedResult(); @@ -1723,12 +1726,14 @@ static class InventoryItem { public InventoryItem() {} public InventoryItem(int id, String item, int qty) { + this.id = id; this.item = item; this.qty = qty; } public InventoryItem(int id, String item, String description, int qty) { + this.id = id; this.item = item; this.description = description; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java index 645e0a66b5..84408bbcb8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationUnitTests.java @@ -24,6 +24,7 @@ import static org.springframework.data.mongodb.test.util.IsBsonObject.*; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.junit.Rule; @@ -31,9 +32,7 @@ import org.junit.rules.ExpectedException; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.test.util.BasicDbListBuilder; -import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObjectBuilder; import com.mongodb.DBObject; @@ -247,25 +246,6 @@ public void expressionBasedFieldsShouldBeReferencableInFollowingOperations() { assertThat(fields.get("foosum"), is((Object) new BasicDBObject("$sum", "$foo"))); } - /** - * @see DATAMONGO-861 - */ - @Test - public void conditionExpressionBasedFieldsShouldBeReferencableInFollowingOperations() { - - DBObject agg = newAggregation( // - project("a"), // - group("a").first(conditional(Criteria.where("a").gte(42), "answer", "no-answer")).as("foosum") // - ).toDbObject("foo", Aggregation.DEFAULT_CONTEXT); - - @SuppressWarnings("unchecked") - DBObject secondProjection = ((List) agg.get("pipeline")).get(1); - DBObject fields = getAsDBObject(secondProjection, "$group"); - assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first")); - assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first.$cond.then", "answer")); - assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first.$cond.else", "no-answer")); - } - /** * @see DATAMONGO-908 */ @@ -331,15 +311,16 @@ public void shouldRenderAggregationWithCustomOptionsCorrectly() { DBObject agg = newAggregation( // project().and("a").as("aa") // ) // - .withOptions(aggregationOptions) // + .withOptions(aggregationOptions) // .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); - assertThat(agg.toString(), is("{ \"aggregate\" : \"foo\" , " // - + "\"pipeline\" : [ { \"$project\" : { \"aa\" : \"$a\"}}] , " // - + "\"allowDiskUse\" : true , " // - + "\"explain\" : true , " // - + "\"cursor\" : { \"foo\" : 1}}" // - )); + assertThat(agg.toString(), + is("{ \"aggregate\" : \"foo\" , " // + + "\"pipeline\" : [ { \"$project\" : { \"aa\" : \"$a\"}}] , " // + + "\"allowDiskUse\" : true , " // + + "\"explain\" : true , " // + + "\"cursor\" : { \"foo\" : 1}}" // + )); } /** @@ -357,8 +338,8 @@ public void shouldSupportReferencingSystemVariables() { ).toDbObject("foo", Aggregation.DEFAULT_CONTEXT); DBObject projection0 = extractPipelineElement(agg, 0, "$project"); - assertThat(projection0, is((DBObject) new BasicDBObject("someKey", 1).append("a1", "$a") - .append("a2", "$$CURRENT.a"))); + assertThat(projection0, + is((DBObject) new BasicDBObject("someKey", 1).append("a1", "$a").append("a2", "$$CURRENT.a"))); DBObject sort = extractPipelineElement(agg, 1, "$sort"); assertThat(sort, is((DBObject) new BasicDBObject("a", -1))); @@ -379,7 +360,7 @@ public void shouldExposeAliasedFieldnameForProjectionsIncludingOperationsDownThe .and("tags").minus(10).as("tags_count")// , group("date")// .sum("tags_count").as("count")// - ).toDbObject("foo", Aggregation.DEFAULT_CONTEXT); + ).toDbObject("foo", Aggregation.DEFAULT_CONTEXT); DBObject group = extractPipelineElement(agg, 1, "$group"); assertThat(getAsDBObject(group, "count"), is(new BasicDBObjectBuilder().add("$sum", "$tags_count").get())); @@ -396,12 +377,31 @@ public void shouldUseAliasedFieldnameForProjectionsIncludingOperationsDownThePip .andExpression("tags-10")// , group("date")// .sum("tags_count").as("count")// - ).toDbObject("foo", Aggregation.DEFAULT_CONTEXT); + ).toDbObject("foo", Aggregation.DEFAULT_CONTEXT); DBObject group = extractPipelineElement(agg, 1, "$group"); assertThat(getAsDBObject(group, "count"), is(new BasicDBObjectBuilder().add("$sum", "$tags_count").get())); } + /** + * @see DATAMONGO-861 + */ + @Test + public void conditionExpressionBasedFieldsShouldBeReferencableInFollowingOperations() { + + DBObject agg = newAggregation( // + project("a"), // + group("a").first(conditional(Criteria.where("a").gte(42), "answer", "no-answer")).as("foosum") // + ).toDbObject("foo", Aggregation.DEFAULT_CONTEXT); + + @SuppressWarnings("unchecked") + DBObject secondProjection = ((List) agg.get("pipeline")).get(1); + DBObject fields = getAsDBObject(secondProjection, "$group"); + assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first")); + assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first.$cond.then", "answer")); + assertThat(getAsDBObject(fields, "foosum"), isBsonObject().containing("$first.$cond.else", "no-answer")); + } + /** * @see DATAMONGO-861 */ @@ -432,7 +432,7 @@ public void shouldRenderProjectionConditionalCorrectly() { DBObject agg = Aggregation.newAggregation(// project().and("color") - .transform(ConditionalOperator.newBuilder() // + .applyCondition(ConditionalOperator.newBuilder() // .when("isYellow") // .then("bright") // .otherwise("dark"))) @@ -456,12 +456,12 @@ public void shouldRenderProjectionConditionalWithCriteriaCorrectly() { DBObject agg = Aggregation .newAggregation(project()// .and("color")// - .transform(conditional(Criteria.where("key").gt(5), "bright", "dark"))) // + .applyCondition(conditional(Criteria.where("key").gt(5), "bright", "dark"))) // .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); DBObject project = extractPipelineElement(agg, 0, "$project"); DBObject expectedCondition = new BasicDBObject() // - .append("if", new BasicDBObject("$gt", new BasicDbListBuilder().add("$key").add(5).get())) // + .append("if", new BasicDBObject("$gt", Arrays. asList("$key", 5))) // .append("then", "bright") // .append("else", "dark"); @@ -478,7 +478,7 @@ public void referencingProjectionAliasesShouldRenderProjectionConditionalWithFie .newAggregation(// project().and("color").as("chroma"), project().and("luminosity") // - .transform(conditional(field("chroma"), "bright", "dark"))) // + .applyCondition(conditional(field("chroma"), "bright", "dark"))) // .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); DBObject project = extractPipelineElement(agg, 1, "$project"); @@ -500,12 +500,12 @@ public void referencingProjectionAliasesShouldRenderProjectionConditionalWithCri .newAggregation(// project().and("color").as("chroma"), project().and("luminosity") // - .transform(conditional(Criteria.where("chroma").is(100), "bright", "dark"))) // + .applyCondition(conditional(Criteria.where("chroma").is(100), "bright", "dark"))) // .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); DBObject project = extractPipelineElement(agg, 1, "$project"); DBObject expectedCondition = new BasicDBObject() // - .append("if", new BasicDBObject("$eq", new BasicDbListBuilder().add("$chroma").add(100).get())) // + .append("if", new BasicDBObject("$eq", Arrays. asList("$chroma", 100))) // .append("then", "bright") // .append("else", "dark"); @@ -522,16 +522,13 @@ public void shouldRenderProjectionIfNullWithFieldReferenceCorrectly() { .newAggregation(// project().and("color"), // project().and("luminosity") // - .transform(ifNull(field("chroma"), "unknown"))) // + .applyCondition(ifNull(field("chroma"), "unknown"))) // .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); DBObject project = extractPipelineElement(agg, 1, "$project"); - BasicDBList expectedCondition = new BasicDbListBuilder() // - .add("$chroma") // - .add("unknown")// - .get(); - assertThat(getAsDBObject(project, "luminosity"), isBsonObject().containing("$ifNull", expectedCondition)); + assertThat(getAsDBObject(project, "luminosity"), + isBsonObject().containing("$ifNull", Arrays. asList("$chroma", "unknown"))); } /** @@ -544,16 +541,13 @@ public void shouldRenderProjectionIfNullWithFallbackFieldReferenceCorrectly() { .newAggregation(// project("fallback").and("color").as("chroma"), project().and("luminosity") // - .transform(ifNull(field("chroma"), field("fallback")))) // + .applyCondition(ifNull(field("chroma"), field("fallback")))) // .toDbObject("foo", Aggregation.DEFAULT_CONTEXT); DBObject project = extractPipelineElement(agg, 1, "$project"); - BasicDBList expectedCondition = new BasicDbListBuilder() // - .add("$chroma") // - .add("$fallback")// - .get(); - assertThat(getAsDBObject(project, "luminosity"), isBsonObject().containing("$ifNull", expectedCondition)); + assertThat(getAsDBObject(project, "luminosity"), + isBsonObject().containing("$ifNull", Arrays.asList("$chroma", "$fallback"))); } private DBObject extractPipelineElement(DBObject agg, int index, String operation) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperatorUnitTests.java index 8eaf380b4e..475559a0a1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperatorUnitTests.java @@ -19,9 +19,10 @@ import static org.springframework.data.mongodb.core.aggregation.ConditionalOperator.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*; +import java.util.Arrays; + import org.junit.Test; import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.test.util.BasicDbListBuilder; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; @@ -30,6 +31,7 @@ * Unit tests for {@link ConditionalOperator}. * * @author Mark Paluch + * @author Christoph Strobl */ public class ConditionalOperatorUnitTests { @@ -117,7 +119,7 @@ public void simpleCriteriaShouldRenderCorrectly() { DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); DBObject expectedCondition = new BasicDBObject() // - .append("if", new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(100).get())) // + .append("if", new BasicDBObject("$gte", Arrays. asList("$luminosity", 100))) // .append("then", "bright") // .append("else", "dark"); @@ -138,14 +140,12 @@ public void andCriteriaShouldRenderCorrectly() { DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); - BasicDBObject luminosity = new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(100).get()); - BasicDBObject hue = new BasicDBObject("$eq", new BasicDbListBuilder().add("$hue").add(50).get()); - BasicDBObject saturation = new BasicDBObject("$lt", new BasicDbListBuilder().add("$saturation").add(11).get()); + BasicDBObject luminosity = new BasicDBObject("$gte", Arrays. asList("$luminosity", 100)); + BasicDBObject hue = new BasicDBObject("$eq", Arrays. asList("$hue", 50)); + BasicDBObject saturation = new BasicDBObject("$lt", Arrays. asList("$saturation", 11)); DBObject expectedCondition = new BasicDBObject() // - .append("if", - new BasicDbListBuilder().add(luminosity) - .add(new BasicDBObject("$and", new BasicDbListBuilder().add(hue).add(saturation).get())).get()) // + .append("if", Arrays. asList(luminosity, new BasicDBObject("$and", Arrays.asList(hue, saturation)))) // .append("then", "bright") // .append("else", "dark"); @@ -164,11 +164,11 @@ public void twoArgsCriteriaShouldRenderCorrectly() { DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); - BasicDBObject gte = new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(100).get()); - BasicDBObject is = new BasicDBObject("$eq", new BasicDbListBuilder().add("$chroma").add(200).get()); + BasicDBObject gte = new BasicDBObject("$gte", Arrays. asList("$luminosity", 100)); + BasicDBObject is = new BasicDBObject("$eq", Arrays. asList("$chroma", 200)); DBObject expectedCondition = new BasicDBObject() // - .append("if", new BasicDbListBuilder().add(gte).add(is).get()) // + .append("if", Arrays.asList(gte, is)) // .append("then", "bright") // .append("else", "dark"); @@ -195,12 +195,12 @@ public void nestedCriteriaShouldRenderCorrectly() { DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); DBObject trueCondition = new BasicDBObject() // - .append("if", new BasicDBObject("$gte", new BasicDbListBuilder().add("$luminosity").add(200).get())) // + .append("if", new BasicDBObject("$gte", Arrays. asList("$luminosity", 200))) // .append("then", "verybright") // .append("else", "not-so-bright"); DBObject falseCondition = new BasicDBObject() // - .append("if", new BasicDBObject("$lt", new BasicDbListBuilder().add("$luminosity").add(50).get())) // + .append("if", new BasicDBObject("$lt", Arrays. asList("$luminosity", 50))) // .append("then", "very-dark") // .append("else", "not-so-dark"); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/IfNullOperatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/IfNullOperatorUnitTests.java index 5ecf68f3a8..a7809644cb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/IfNullOperatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/IfNullOperatorUnitTests.java @@ -19,16 +19,17 @@ import static org.junit.Assert.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*; +import java.util.Arrays; + import org.junit.Test; -import org.springframework.data.mongodb.test.util.BasicDbListBuilder; -import com.mongodb.BasicDBList; import com.mongodb.DBObject; /** * Unit tests for {@link IfNullOperator}. * * @author Mark Paluch + * @author Christoph Strobl */ public class IfNullOperatorUnitTests { @@ -60,12 +61,8 @@ public void simpleIfNullShouldRenderCorrectly() { DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); - BasicDBList expected = new BasicDbListBuilder() // - .add("$optional") // - .add("a more sophisticated value")// - .get(); - - assertThat(dbObject, isBsonObject().containing("$ifNull", expected)); + assertThat(dbObject, + isBsonObject().containing("$ifNull", Arrays. asList("$optional", "a more sophisticated value"))); } /** @@ -80,11 +77,6 @@ public void fieldReplacementIfNullShouldRenderCorrectly() { DBObject dbObject = operator.toDbObject(Aggregation.DEFAULT_CONTEXT); - BasicDBList expected = new BasicDbListBuilder() // - .add("$optional") // - .add("$never-null")// - .get(); - - assertThat(dbObject, isBsonObject().containing("$ifNull", expected)); + assertThat(dbObject, isBsonObject().containing("$ifNull", Arrays. asList("$optional", "$never-null"))); } } 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 a0322a5880..b1371c00c3 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 @@ -291,7 +291,7 @@ public void rendersAggregationConditionalInTypedAggregationContextCorrectly() { TypedAggregation agg = newAggregation(FooPerson.class, project("name") // .and("age") // - .transform(conditional(Criteria.where("age.value").lt(10), new Age(0), field("age"))) // + .applyCondition(conditional(Criteria.where("age.value").lt(10), new Age(0), field("age"))) // ); DBObject dbo = agg.toDbObject("person", context); @@ -317,7 +317,7 @@ public void rendersAggregationIfNullInTypedAggregationContextCorrectly() { TypedAggregation agg = newAggregation(FooPerson.class, project("name") // .and("age") // - .transform(ifNull("age", new Age(0))) // + .applyCondition(ifNull("age", new Age(0))) // ); DBObject dbo = agg.toDbObject("person", context); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java index d42cec2845..9857ad379b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java @@ -83,7 +83,15 @@ protected boolean matchesSafely(T item) { } if (expectation.type != null && !ClassUtils.isAssignable(expectation.type, o.getClass())) { - return false; + + if (o instanceof List) { + if (!ClassUtils.isAssignable(List.class, expectation.type)) { + return false; + } + } else { + return false; + } + } if (expectation.value != null && !new IsEqual(expectation.value).matches(o)) { diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 3d396a4dbd..c8b1553e41 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1988,8 +1988,8 @@ List resultList = result.getMappedResults(); Note that we can also refer to other fields of the document within the SpEL expression. -[[mongo.aggregation.examples.example6]] -.Aggregation Framework Example 6 +[[mongo.aggregation.examples.example7]] +.Aggregation Framework Example 7 This example uses conditional projection. It's derived from the https://docs.mongodb.com/manual/reference/operator/aggregation/cond/[$cond reference documentation]. @@ -2019,7 +2019,7 @@ import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; TypedAggregation agg = newAggregation(InventoryItem.class, project("item").and("discount") - .transform(ConditionalOperator.newBuilder().when(Criteria.where("qty").gte(250)) + .applyCondition(ConditionalOperator.newBuilder().when(Criteria.where("qty").gte(250)) .then(30) .otherwise(20)) .and(ifNull("description", "Unspecified")).as("description") @@ -2029,7 +2029,7 @@ AggregationResults result = mongoTemplate.aggregate(agg List stateStatsList = result.getMappedResults(); ---- -* This one-step aggregation uses a projection operation with the `inventory` collection. We project the `discount` field using a conditional transformation for all inventory items that have a `qty` greater or equal to `250`. An second conditional projection is performed for the `description` field. We apply the description `Unspecified` to all items that either do not have a `description` field of items that have a `null` description. +* This one-step aggregation uses a projection operation with the `inventory` collection. We project the `discount` field using a conditional operation for all inventory items that have a `qty` greater or equal to `250`. An second conditional projection is performed for the `description` field. We apply the description `Unspecified` to all items that either do not have a `description` field of items that have a `null` description. [[mongo.custom-converters]]