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
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..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) {
@@ -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..881894a14f
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperator.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.core.aggregation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.mongodb.core.query.CriteriaDefinition;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+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
+ * @author Christoph Strobl
+ * @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);
+ }
+
+ return context.getMappedObject(new BasicDBObject("$set", value)).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());
+ List clauses = new ArrayList();
+
+ 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 List getClauses(AggregationOperationContext context, DBObject mappedObject) {
+
+ List clauses = new ArrayList();
+
+ for (String key : mappedObject.keySet()) {
+
+ Object predicate = mappedObject.get(key);
+ clauses.addAll(getClauses(context, key, predicate));
+ }
+
+ return clauses;
+ }
+
+ private List getClauses(AggregationOperationContext context, String key, Object predicate) {
+
+ List clauses = new ArrayList();
+
+ if (predicate instanceof List) {
+
+ 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));
+
+ } else if (predicate instanceof DBObject) {
+
+ DBObject nested = (DBObject) predicate;
+
+ for (String s : nested.keySet()) {
+
+ if (!isKeyword(s)) {
+ continue;
+ }
+
+ List args = new ArrayList();
+ args.add("$" + key);
+ args.add(nested.get(s));
+ clauses.add(new BasicDBObject(s, args));
+ }
+
+ } else if (!isKeyword(key)) {
+
+ List args = new ArrayList();
+ 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();
+ }
+
+ /**
+ * @since 1.10
+ */
+ 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);
+ }
+
+ /**
+ * @since 1.10
+ */
+ 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);
+ }
+
+ /**
+ * @since 1.10
+ */
+ 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..b51aba01bb
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/IfNullOperator.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.data.mongodb.core.aggregation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.util.Assert;
+
+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) {
+
+ List list = new ArrayList();
+
+ list.add(context.getReference(field).toString());
+ list.add(resolve(value, context));
+
+ return new BasicDBObject("$ifNull", list);
+ }
+
+ 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 context.getMappedObject(new BasicDBObject("$set", value)).get("$set");
+ }
+
+ /**
+ * Get a builder that allows fluent creation of {@link IfNullOperator}.
+ *
+ * @return a new {@link IfNullBuilder}.
+ */
+ public static IfNullBuilder newBuilder() {
+ return IfNullOperatorBuilder.newBuilder();
+ }
+
+ /**
+ * @since 1.10
+ */
+ 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);
+ }
+
+ /**
+ * @since 1.10
+ */
+ 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..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 {
@@ -104,8 +105,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 +241,24 @@ public DBObject toDBObject(AggregationOperationContext context) {
* @return
*/
public abstract ProjectionOperation as(String alias);
+
+ /**
+ * Apply a conditional projection using {@link ConditionalOperator}.
+ *
+ * @param conditionalOperator must not be {@literal null}.
+ * @return never {@literal null}.
+ * @since 1.10
+ */
+ public abstract ProjectionOperation applyCondition(ConditionalOperator conditionalOperator);
+
+ /**
+ * Apply a conditional value replacement for {@literal null} values using {@link IfNullOperator}.
+ *
+ * @param ifNullOperator must not be {@literal null}.
+ * @return never {@literal null}.
+ * @since 1.10
+ */
+ public abstract ProjectionOperation applyCondition(IfNullOperator ifNullOperator);
}
/**
@@ -341,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);
}
}
@@ -370,7 +390,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 +440,7 @@ public ProjectionOperation nested(Fields fields) {
/**
* Allows to specify an alias for the previous projection operation.
*
- * @param string
+ * @param alias
* @return
*/
@Override
@@ -436,6 +457,28 @@ 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 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 applyCondition(IfNullOperator ifNullOperator) {
+
+ Assert.notNull(ifNullOperator, "IfNullOperator must not be null!");
+ return this.operation.and(new ExpressionProjection(Fields.field(name), ifNullOperator));
+ }
+
/**
* Generates an {@code $add} expression that adds the given number to the previously mentioned field.
*
@@ -764,7 +807,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..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
@@ -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")//
+ .applyCondition(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")//
+ .applyCondition(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")//
+ .applyCondition(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")//
+ .applyCondition(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")//
+ .applyCondition(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
*/
@@ -550,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));
@@ -643,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
@@ -666,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
*/
@@ -696,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
*/
@@ -731,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);
@@ -762,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);
@@ -884,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();
@@ -1460,4 +1711,33 @@ 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..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
@@ -19,16 +19,19 @@
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.*;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import org.junit.Rule;
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 com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
@@ -308,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}}" //
+ ));
}
/**
@@ -334,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)));
@@ -356,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()));
@@ -373,12 +377,179 @@ 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
+ */
+ @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")
+ .applyCondition(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")//
+ .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", Arrays. asList("$key", 5))) //
+ .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") //
+ .applyCondition(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") //
+ .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", Arrays. asList("$chroma", 100))) //
+ .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") //
+ .applyCondition(ifNull(field("chroma"), "unknown"))) //
+ .toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
+
+ DBObject project = extractPipelineElement(agg, 1, "$project");
+
+ assertThat(getAsDBObject(project, "luminosity"),
+ isBsonObject().containing("$ifNull", Arrays. asList("$chroma", "unknown")));
+ }
+
+ /**
+ * @see DATAMONGO-861
+ */
+ @Test
+ public void shouldRenderProjectionIfNullWithFallbackFieldReferenceCorrectly() {
+
+ DBObject agg = Aggregation
+ .newAggregation(//
+ project("fallback").and("color").as("chroma"),
+ project().and("luminosity") //
+ .applyCondition(ifNull(field("chroma"), field("fallback")))) //
+ .toDbObject("foo", Aggregation.DEFAULT_CONTEXT);
+
+ DBObject project = extractPipelineElement(agg, 1, "$project");
+
+ assertThat(getAsDBObject(project, "luminosity"),
+ isBsonObject().containing("$ifNull", Arrays.asList("$chroma", "$fallback")));
+ }
+
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..475559a0a1
--- /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 java.util.Arrays;
+
+import org.junit.Test;
+import org.springframework.data.mongodb.core.query.Criteria;
+
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBObject;
+
+/**
+ * Unit tests for {@link ConditionalOperator}.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ */
+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", Arrays. asList("$luminosity", 100))) //
+ .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", 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", Arrays. asList(luminosity, new BasicDBObject("$and", Arrays.asList(hue, saturation)))) //
+ .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", Arrays. asList("$luminosity", 100));
+ BasicDBObject is = new BasicDBObject("$eq", Arrays. asList("$chroma", 200));
+
+ DBObject expectedCondition = new BasicDBObject() //
+ .append("if", Arrays.asList(gte, is)) //
+ .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", Arrays. asList("$luminosity", 200))) //
+ .append("then", "verybright") //
+ .append("else", "not-so-bright");
+
+ DBObject falseCondition = new BasicDBObject() //
+ .append("if", new BasicDBObject("$lt", Arrays. asList("$luminosity", 50))) //
+ .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..a7809644cb
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/IfNullOperatorUnitTests.java
@@ -0,0 +1,82 @@
+/*
+ * 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 java.util.Arrays;
+
+import org.junit.Test;
+
+import com.mongodb.DBObject;
+
+/**
+ * Unit tests for {@link IfNullOperator}.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ */
+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);
+
+ assertThat(dbObject,
+ isBsonObject().containing("$ifNull", Arrays. asList("$optional", "a more sophisticated value")));
+ }
+
+ /**
+ * @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);
+
+ 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 eece18bca2..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
@@ -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") //
+ .applyCondition(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") //
+ .applyCondition(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/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/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..c8b1553e41 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.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].
+
+[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")
+ .applyCondition(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 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]]
== Overriding default mapping with custom converters