diff --git a/pom.xml b/pom.xml
index 75745189ed..0c10dd7569 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-mongodb-parent
- 1.7.0.BUILD-SNAPSHOT
+ 1.7.0.DATAMONGO-941-SNAPSHOT
pom
Spring Data MongoDB
diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml
index 40e8a0f253..c8499cce81 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.7.0.BUILD-SNAPSHOT
+ 1.7.0.DATAMONGO-941-SNAPSHOT
../pom.xml
@@ -48,7 +48,7 @@
org.springframework.data
spring-data-mongodb
- 1.7.0.BUILD-SNAPSHOT
+ 1.7.0.DATAMONGO-941-SNAPSHOT
diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml
index 13110137b6..4b7d045329 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.7.0.BUILD-SNAPSHOT
+ 1.7.0.DATAMONGO-941-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb-log4j/pom.xml b/spring-data-mongodb-log4j/pom.xml
index 6ff09e4577..55431e528b 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.7.0.BUILD-SNAPSHOT
+ 1.7.0.DATAMONGO-941-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml
index bfcacd66eb..e5d331fc19 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.7.0.BUILD-SNAPSHOT
+ 1.7.0.DATAMONGO-941-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
index 55274b37c1..761684de5c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
@@ -15,10 +15,15 @@
*/
package org.springframework.data.mongodb.core.convert;
+import static org.springframework.data.mongodb.core.convert.QueryMapper.KeywordFactory.*;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
@@ -27,6 +32,7 @@
import org.springframework.core.convert.ConversionException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PropertyPath;
@@ -39,6 +45,13 @@
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty.PropertyToFieldNameConverter;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.validation.Errors;
+import org.springframework.validation.MapBindingResult;
+import org.springframework.validation.ObjectError;
+import org.springframework.validation.Validator;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
@@ -93,7 +106,7 @@ public QueryMapper(MongoConverter converter) {
public DBObject getMappedObject(DBObject query, MongoPersistentEntity> entity) {
if (isNestedKeyword(query)) {
- return getMappedKeyword(new Keyword(query), entity);
+ return getMappedKeyword(keywordFor(query, entity, null, mappingContext), entity);
}
DBObject result = new BasicDBObject();
@@ -111,7 +124,7 @@ public DBObject getMappedObject(DBObject query, MongoPersistentEntity> entity)
}
if (isKeyword(key)) {
- result.putAll(getMappedKeyword(new Keyword(query, key), entity));
+ result.putAll(getMappedKeyword(keywordFor(key, query.get(key), entity, null, mappingContext), entity));
continue;
}
@@ -192,7 +205,7 @@ protected Entry getMappedObjectForField(Field field, Object rawV
Object value;
if (isNestedKeyword(rawValue) && !field.isIdField()) {
- Keyword keyword = new Keyword((DBObject) rawValue);
+ Keyword keyword = keywordFor((DBObject) rawValue, field.getProperty(), mappingContext);
value = getMappedKeyword(field, keyword);
} else {
value = getMappedValue(field, rawValue);
@@ -221,6 +234,8 @@ protected Field createPropertyField(MongoPersistentEntity> entity, String key,
*/
protected DBObject getMappedKeyword(Keyword keyword, MongoPersistentEntity> entity) {
+ keyword.validate();
+
// $or/$nor
if (keyword.isOrOrNor() || keyword.hasIterableValue()) {
@@ -294,7 +309,7 @@ protected Object getMappedValue(Field documentField, Object value) {
}
if (isNestedKeyword(value)) {
- return getMappedKeyword(new Keyword((DBObject) value), null);
+ return getMappedKeyword(keywordFor((DBObject) value, documentField.getProperty(), mappingContext), null);
}
if (isAssociationConversionNecessary(documentField, value)) {
@@ -510,26 +525,22 @@ protected boolean isKeyword(String candidate) {
* Value object to capture a query keyword representation.
*
* @author Oliver Gierke
+ * @author Christoph Strobl
*/
static class Keyword {
private static final String N_OR_PATTERN = "\\$.*or";
-
private final String key;
private final Object value;
+ private final KeywordContext context;
+ private final List validators;
- public Keyword(DBObject source, String key) {
- this.key = key;
- this.value = source.get(key);
- }
-
- public Keyword(DBObject dbObject) {
-
- Set keys = dbObject.keySet();
- Assert.isTrue(keys.size() == 1, "Can only use a single value DBObject!");
+ public Keyword(String key, Object value, KeywordContext context, List validators) {
- this.key = keys.iterator().next();
- this.value = dbObject.get(key);
+ this.key = key;
+ this.value = value;
+ this.context = context;
+ this.validators = validators;
}
/**
@@ -549,13 +560,296 @@ public boolean hasIterableValue() {
return value instanceof Iterable;
}
+ boolean isDBObjectValue() {
+ return value instanceof DBObject;
+ }
+
public String getKey() {
return key;
}
@SuppressWarnings("unchecked")
public T getValue() {
- return (T) value;
+ return (T) this.value;
+ }
+
+ public KeywordContext getContext() {
+ return context;
+ }
+
+ /**
+ * Validate the keyword within the boundary of its {@link KeywordContext}.
+ *
+ * @since 1.7
+ */
+ @SuppressWarnings("rawtypes")
+ public void validate() {
+
+ if (CollectionUtils.isEmpty(validators)) {
+ return;
+ }
+
+ MapBindingResult validationResult = new MapBindingResult(new LinkedHashMap(), this.key);
+ for (Validator validator : validators) {
+
+ if (validator.supports(Keyword.class)) {
+ validator.validate(this, validationResult);
+ }
+
+ if (this.value == null) {
+ continue;
+ }
+
+ if (validator.supports(this.value.getClass())) {
+ validator.validate(context, validationResult);
+ }
+ }
+
+ if (!validationResult.hasErrors()) {
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (ObjectError error : validationResult.getAllErrors()) {
+ sb.append(error.getDefaultMessage() + "\r\n");
+ }
+
+ throw new InvalidDataAccessApiUsageException(sb.toString());
+ }
+ }
+
+ /**
+ * Wrapper to simplify usage of {@link Validator}s.
+ *
+ * @author Christoph Strobl
+ * @since 1.7
+ */
+ static class KeywordContext {
+
+ private final MongoPersistentProperty property;
+ private final MongoPersistentEntity> entity;
+ private final MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext;
+
+ public KeywordContext(MongoPersistentProperty property, MongoPersistentEntity> entity,
+ MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext) {
+ this.property = property;
+ this.entity = entity;
+ this.mappingContext = mappingContext;
+ }
+
+ public MongoPersistentEntity> getEntity() {
+ return entity;
+ }
+
+ public MongoPersistentProperty getProperty() {
+ return property;
+ }
+
+ public MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> getMappingContext() {
+ return mappingContext;
+ }
+ }
+
+ /**
+ * Creates {@link Keyword} and sets the {@link KeywordContext}. Also registers {@link Validator}s for specific
+ * keywords.
+ *
+ * @author Christoph Strobl
+ * @since 1.7
+ */
+ static enum KeywordFactory {
+
+ INSTANCE;
+
+ private static final Validator VALID_MIN_OPERATOR_TYPES_VALIDATOR = KeywordParameterTypeValidator.whitelist(
+ Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Date.class);
+
+ static Keyword keywordFor(DBObject source,
+ MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext) {
+ return keywordFor(source, (MongoPersistentEntity>) null, null, mappingContext);
+ }
+
+ static Keyword keywordFor(DBObject source, MongoPersistentProperty property,
+ MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext) {
+ return keywordFor(source, null, property, mappingContext);
+ }
+
+ static Keyword keywordFor(DBObject source, MongoPersistentEntity> entity, MongoPersistentProperty property,
+ MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext) {
+
+ String key = assertAndReturnOnlySingleKey(source);
+ return keywordFor(key, source.get(key), entity, property, mappingContext);
+ }
+
+ static Keyword keywordFor(String key, Object value, MongoPersistentEntity> entity,
+ MongoPersistentProperty property,
+ MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext) {
+
+ KeywordContext context = new KeywordContext(property, property == null ? entity
+ : (MongoPersistentEntity>) property.getOwner(), mappingContext);
+
+ return new Keyword(key, value, context, INSTANCE.getValidatorsFor(key));
+ }
+
+ private List getValidatorsFor(String key) {
+
+ if (key.equalsIgnoreCase("$min")) {
+ return Arrays.asList(VALID_MIN_OPERATOR_TYPES_VALIDATOR);
+ }
+
+ return Collections. emptyList();
+ }
+
+ static String assertAndReturnOnlySingleKey(DBObject dbo) {
+
+ Set keys = dbo.keySet();
+ Assert.isTrue(keys.size() == 1, "Can only use a single value DBObject!");
+
+ return keys.iterator().next();
+ }
+ }
+
+ /**
+ * @author Christoph Strobl
+ * @since 1.7
+ */
+ static abstract class KeywordValidator implements Validator {
+
+ @Override
+ public boolean supports(Class> clazz) {
+ return ClassUtils.isAssignable(Keyword.class, clazz);
+ }
+
+ public void validate(Object target, Errors errors) {
+ validate((Keyword) target, errors);
+ }
+
+ public abstract void validate(Keyword keyword, Errors errors);
+
+ }
+
+ /**
+ * {@link Validator} checking type attributes for keyowords on the corresponding value and
+ * {@link MongoPersistentProperty}. In case the value to check is a {@link DBObject} all nested values will be
+ * recoursively checked.
+ *
+ * @author Christoph Strobl
+ * @since 1.7
+ */
+ static class KeywordParameterTypeValidator extends KeywordValidator {
+
+ enum Mode {
+ WHITE_LIST, BLACK_LIST;
+ }
+
+ private final Set> types;
+ private final Mode mode;
+
+ private KeywordParameterTypeValidator(Mode mode, Class>... supportedTypes) {
+
+ this.mode = mode;
+ this.types = new HashSet>(Arrays.asList(supportedTypes));
+ }
+
+ public static KeywordParameterTypeValidator whitelist(Class>... supportedTypes) {
+ return new KeywordParameterTypeValidator(Mode.WHITE_LIST, supportedTypes);
+ }
+
+ public static KeywordParameterTypeValidator blacklist(Class>... unsupportedTypes) {
+ return new KeywordParameterTypeValidator(Mode.BLACK_LIST, unsupportedTypes);
+ }
+
+ private void doValidate(Object candidate, Errors errors) {
+
+ if (types.isEmpty() || candidate == null) {
+ return;
+ }
+
+ if (candidate instanceof DBObject) {
+ DBObject value = (DBObject) candidate;
+
+ Iterator> it = value.toMap().keySet().iterator();
+ while (it.hasNext()) {
+ doValidate(value.get(it.next().toString()), errors);
+ }
+
+ return;
+ }
+
+ Class> typeToValidate = ClassUtils.isAssignable(MongoPersistentProperty.class, candidate.getClass()) ? ((MongoPersistentProperty) candidate)
+ .getActualType() : candidate.getClass();
+ boolean givenTypeContainedInTypes = types.contains(ClassUtils.resolvePrimitiveIfNecessary(typeToValidate));
+
+ if ((Mode.BLACK_LIST.equals(mode) && givenTypeContainedInTypes)
+ || (Mode.WHITE_LIST.equals(mode) && !givenTypeContainedInTypes)) {
+ errors.reject("", String.format("Using %s is not supported for %s.", typeToValidate, errors.getObjectName()));
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mongodb.core.convert.QueryMapper.KeywordValidator#validate(org.springframework.data.mongodb.core.convert.QueryMapper.KeywordContext, org.springframework.validation.Errors)
+ */
+ @Override
+ public void validate(Keyword target, Errors errors) {
+
+ if (types.isEmpty() || target == null) {
+ return;
+ }
+
+ if (!target.isDBObjectValue()) {
+
+ MongoPersistentProperty propertyToUse = target.getContext().getProperty();
+
+ if (propertyToUse == null && target.getContext().getEntity() != null) {
+ propertyToUse = target.getContext().getEntity().getPersistentProperty(errors.getObjectName());
+ }
+
+ doValidate(propertyToUse, errors);
+ return;
+ }
+
+ DBObject dbo = (DBObject) target.getValue();
+
+ Iterator> keysIterator = dbo.keySet().iterator();
+ while (keysIterator.hasNext()) {
+ validatePropertyPath(keysIterator.next().toString(), target.getContext(), errors);
+ }
+ }
+
+ private void validatePropertyPath(String propertyPath, KeywordContext context, Errors errors) {
+
+ if (StringUtils.countOccurrencesOf(propertyPath, ".") == 0) {
+ doValidate(context.getEntity().getPersistentProperty(propertyPath), errors);
+ return;
+ }
+
+ MongoPersistentEntity> persistentEntity = context.getEntity();
+
+ Iterator partsIterator = Arrays.asList(propertyPath.split("\\.")).iterator();
+ while (partsIterator.hasNext()) {
+
+ MongoPersistentProperty persistentProperty = (MongoPersistentProperty) persistentEntity
+ .getPersistentProperty(partsIterator.next());
+
+ if (persistentProperty == null) {
+ continue;
+ }
+
+ if (!partsIterator.hasNext()) {
+ doValidate(persistentProperty, errors);
+ return;
+ }
+
+ if (persistentProperty.isEntity() && partsIterator.hasNext()) {
+
+ persistentEntity = context.getMappingContext().getPersistentEntity(persistentProperty.getActualType());
+
+ if (persistentEntity == null) {
+ return;
+ }
+ }
+ }
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java
index a4429cab91..c85ec73881 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java
@@ -20,6 +20,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -308,6 +309,38 @@ public Update currentTimestamp(String key) {
return this;
}
+ /**
+ * Update given key to value if the given value is less than the current value of the field.
+ *
+ * @see http://docs.mongodb.org/manual/reference/operator/update/min/
+ * @param key must not be {@literal null}.
+ * @param value must not be {@literal null}.
+ * @return
+ * @since 1.7
+ */
+ public Update min(String key, Number value) {
+
+ Assert.notNull(value, "Value for min operation must not be 'null'.");
+ addMultiFieldOperation("$min", key, value);
+ return this;
+ }
+
+ /**
+ * Update given key to value if the given date is before than the current date value of the field.
+ *
+ * @see http://docs.mongodb.org/manual/reference/operator/update/min/
+ * @param key must not be {@literal null}.
+ * @param value must not be {@literal null}.
+ * @return
+ * @since 1.7
+ */
+ public Update min(String key, Date value) {
+
+ Assert.notNull(value, "Value for min operation must not be 'null'.");
+ addMultiFieldOperation("$min", key, value);
+ return this;
+ }
+
public DBObject getUpdateObject() {
DBObject dbo = new BasicDBObject();
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectTestUtils.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectTestUtils.java
index f35391e626..28ee33216e 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectTestUtils.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DBObjectTestUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012 the original author or authors.
+ * Copyright 2012 - 2014 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.
@@ -18,6 +18,10 @@
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
import com.mongodb.BasicDBList;
import com.mongodb.DBObject;
@@ -25,6 +29,7 @@
* Helper classes to ease assertions on {@link DBObject}s.
*
* @author Oliver Gierke
+ * @author Christoph Strobl
*/
public abstract class DBObjectTestUtils {
@@ -32,6 +37,51 @@ private DBObjectTestUtils() {
}
+ /**
+ * Extracts value for a given path within the dbo. Indexes in arrays can be addressed via {@code []}.
+ *
+ * @param source
+ * @param path
+ * @return
+ */
+ @SuppressWarnings("unchecked")
+ public static T getValue(DBObject source, String path) {
+
+ String[] fragments = path.split("\\.");
+ if (fragments.length == 1) {
+ return (T) source.get(path);
+ }
+
+ Iterator it = Arrays.asList(fragments).iterator();
+
+ DBObject dbo = source;
+ while (it.hasNext()) {
+
+ String key = it.next();
+
+ if (key.startsWith("[")) {
+ String indexNumber = key.substring(1, key.indexOf("]"));
+ dbo = getAsDBObject((BasicDBList) dbo, Integer.parseInt(indexNumber));
+ } else {
+
+ if (!it.hasNext()) {
+ return (T) dbo.get(key);
+ }
+
+ Object value = dbo.get(key);
+ if (value instanceof DBObject) {
+ dbo = (DBObject) value;
+ } else {
+ if (it.next().startsWith("$")) {
+ return (T) value;
+ }
+ }
+ }
+ }
+
+ throw new NoSuchElementException(String.format("Unable to find '%s' in %s.", path, source));
+ }
+
/**
* Expects the field with the given key to be not {@literal null} and a {@link DBObject} in turn and returns it.
*
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
index a6076cb2f9..99f911d755 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
@@ -23,14 +23,17 @@
import static org.springframework.data.mongodb.core.query.Query.*;
import static org.springframework.data.mongodb.core.query.Update.*;
+import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import org.bson.types.ObjectId;
@@ -90,7 +93,7 @@
/**
* Integration test for {@link MongoTemplate}.
- *
+ *
* @author Oliver Gierke
* @author Thomas Risberg
* @author Amol Nayak
@@ -2740,6 +2743,65 @@ public void insertsAndRemovesBasicDbObjectCorrectly() {
assertThat(template.findAll(DBObject.class, "collection"), hasSize(0));
}
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void updatesDateValueCorrectlyWhenUsingMinOperator() {
+
+ Calendar cal = Calendar.getInstance(Locale.US);
+ cal.set(2013, 10, 13, 0, 0, 0);
+
+ TypeWithDate twd = new TypeWithDate();
+ twd.date = new Date();
+ template.save(twd);
+ template.updateFirst(query(where("id").is(twd.id)), new Update().min("date", cal.getTime()), TypeWithDate.class);
+
+ TypeWithDate loaded = template.find(query(where("id").is(twd.id)), TypeWithDate.class).get(0);
+ assertThat(loaded.date, equalTo(cal.getTime()));
+ }
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void updatesNumericValueCorrectlyWhenUsingMinOperator() {
+
+ TypeWithNumbers twn = new TypeWithNumbers();
+ twn.byteVal = 100;
+ twn.doubleVal = 200D;
+ twn.floatVal = 300F;
+ twn.intVal = 400;
+ twn.longVal = 500L;
+
+ // Note that $min operator is not supported for BigInteger and BigDecimal types.
+ // twn.bigIntegerVal = new BigInteger("600");
+ // twn.bigDeciamVal = new BigDecimal("700.0");
+
+ template.save(twn);
+
+ byte byteVal = 90;
+ Update update = new Update()//
+ .min("byteVal", byteVal) //
+ .min("doubleVal", 190D) //
+ .min("floatVal", 290F) //
+ .min("intVal", 390) //
+ .min("longVal", 490) //
+ // Not supported
+ // .min("bigIntegerVal", new BigInteger("590")) //
+ // .min("bigDeciamVal", new BigDecimal("690")) //
+ ;
+
+ template.updateFirst(query(where("id").is(twn.id)), update, TypeWithNumbers.class);
+
+ TypeWithNumbers loaded = template.find(query(where("id").is(twn.id)), TypeWithNumbers.class).get(0);
+ assertThat(loaded.byteVal, equalTo(byteVal));
+ assertThat(loaded.doubleVal, equalTo(190D));
+ assertThat(loaded.floatVal, equalTo(290F));
+ assertThat(loaded.intVal, equalTo(390));
+ assertThat(loaded.longVal, equalTo(490L));
+ }
+
static class DoucmentWithNamedIdField {
@Id String someIdKey;
@@ -3007,4 +3069,16 @@ static class SomeMessage {
@org.springframework.data.mongodb.core.mapping.DBRef SomeContent dbrefContent;
SomeContent normalContent;
}
+
+ static class TypeWithNumbers {
+
+ @Id String id;
+ Integer intVal;
+ Float floatVal;
+ Long longVal;
+ Double doubleVal;
+ BigDecimal bigDeciamVal;
+ BigInteger bigIntegerVal;
+ Byte byteVal;
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java
index 94af26d3d0..14a720c15e 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java
@@ -21,6 +21,8 @@
import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.DBObjectTestUtils.*;
+import java.math.BigDecimal;
+import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
@@ -28,12 +30,15 @@
import org.hamcrest.collection.IsIterableContainingInOrder;
import org.hamcrest.core.IsEqual;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.convert.converter.Converter;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mapping.model.MappingException;
@@ -61,6 +66,8 @@
@RunWith(MockitoJUnitRunner.class)
public class UpdateMapperUnitTests {
+ public @Rule ExpectedException exception = ExpectedException.none();
+
@Mock MongoDbFactory factory;
MappingMongoConverter converter;
MongoMappingContext context;
@@ -68,7 +75,6 @@ public class UpdateMapperUnitTests {
private Converter writingConverterSpy;
- @SuppressWarnings("unchecked")
@Before
public void setUp() {
@@ -525,6 +531,120 @@ public void shouldNotRemovePositionalParameter() {
assertThat($unset, equalTo(new BasicDBObjectBuilder().add("dbRefAnnotatedList.$", 1).get()));
}
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void shouldMapMinCorrectlyWhenGivenDouble() {
+
+ Update update = new Update().min("doubleVal", 10D);
+
+ DBObject mapped = mapper.getMappedObject(update.getUpdateObject(),
+ context.getPersistentEntity(DocumentWithNumbers.class));
+
+ assertThat(DBObjectTestUtils. getValue(mapped, "$min.doubleVal"), equalTo(10D));
+ }
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void shouldMapMinCorrectlyWhenReferencingNonExistingProperty() {
+
+ Update update = new Update().min("xyz", 10D);
+
+ DBObject mapped = mapper.getMappedObject(update.getUpdateObject(),
+ context.getPersistentEntity(DocumentWithNumbers.class));
+
+ assertThat(DBObjectTestUtils. getValue(mapped, "$min.xyz"), equalTo(10D));
+ }
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void shouldThrowErrorWhenMappingMinWithBigDecimal() {
+
+ exception.expect(InvalidDataAccessApiUsageException.class);
+ exception.expectMessage(BigDecimal.class.getName() + " is not supported for $min");
+
+ Update update = new Update().min("bigDeciamVal", new BigDecimal(100));
+
+ mapper.getMappedObject(update.getUpdateObject(), context.getPersistentEntity(DocumentWithNumbers.class));
+ }
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void shouldThrowErrorWhenMappingMinToPropertyThatIsBigDecimal() {
+
+ exception.expect(InvalidDataAccessApiUsageException.class);
+ exception.expectMessage(BigDecimal.class.getName() + " is not supported for $min");
+
+ Update update = new Update().min("bigDeciamVal", 10);
+
+ mapper.getMappedObject(update.getUpdateObject(), context.getPersistentEntity(DocumentWithNumbers.class));
+ }
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void shouldMapMinCorrectlyWhenMappingMinToPropertyThatIsPrimitiveValue() {
+
+ Update update = new Update().min("primitiveIntVal", 10);
+
+ DBObject mapped = mapper.getMappedObject(update.getUpdateObject(),
+ context.getPersistentEntity(DocumentWithNumbers.class));
+
+ assertThat(DBObjectTestUtils. getValue(mapped, "$min.primitiveIntVal"), equalTo(10));
+ }
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void shouldMapMinOnNestedPropertyCorrectly() {
+
+ Update update = new Update().min("nested.primitiveIntVal", 10);
+
+ DBObject mapped = mapper.getMappedObject(update.getUpdateObject(),
+ context.getPersistentEntity(DocumentWithNumbersWrapper.class));
+
+ DBObject $min = DBObjectTestUtils.getValue(mapped, "$min");
+ assertThat((Integer) $min.get("nested.primitiveIntVal"), is(10));
+ }
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void shouldThrowErrorWhenMappingMinToNestedPropertyThatIsBigDecimal() {
+
+ exception.expect(InvalidDataAccessApiUsageException.class);
+ exception.expectMessage(BigDecimal.class.getName() + " is not supported for $min");
+
+ Update update = new Update().min("nested.bigDeciamVal", 10);
+
+ mapper.getMappedObject(update.getUpdateObject(), context.getPersistentEntity(DocumentWithNumbersWrapper.class));
+ }
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void shouldMapMinOnNestedPropertyThatDoesNotExistCorrectly() {
+
+ Update update = new Update().min("nested.xyz", 10);
+
+ DBObject mapped = mapper.getMappedObject(update.getUpdateObject(),
+ context.getPersistentEntity(DocumentWithNumbersWrapper.class));
+
+ DBObject $min = DBObjectTestUtils.getValue(mapped, "$min");
+ assertThat((Integer) $min.get("nested.xyz"), is(10));
+ }
+
@org.springframework.data.mongodb.core.mapping.Document(collection = "DocumentWithReferenceToInterface")
static interface DocumentWithReferenceToInterface {
@@ -700,4 +820,25 @@ static class Wrapper {
@Field("mapped") DocumentWithDBRefCollection nested;
}
+
+ static class DocumentWithNumbersWrapper {
+ DocumentWithNumbers nested;
+ }
+
+ static class DocumentWithNumbers {
+
+ BigInteger bigIntVal;
+ BigDecimal bigDeciamVal;
+ Byte byteVal;
+ Double doubleVal;
+ Float floatVal;
+ Integer intVal;
+ Long longVal;
+
+ byte primitiveByteVal;
+ double primitiveDoubleVal;
+ float primitiveFloatVal;
+ int primitiveIntVal;
+ long primitiveLongVal;
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/UpdateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/UpdateTests.java
index 900a5b7011..3e78f94140 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/UpdateTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/UpdateTests.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.*;
import java.util.Collections;
+import java.util.Date;
import java.util.Map;
import org.joda.time.DateTime;
@@ -420,4 +421,45 @@ public void toStringWorksForUpdateWithComplexObject() {
Update update = new Update().addToSet("key", new DateTime());
assertThat(update.toString(), is(notNullValue()));
}
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void minWithNumberShouldThrowExceptionWhenGivenNullValue() {
+ new Update().min("key", (Number) null);
+ }
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void minWithDateShouldThrowExceptionWhenGivenNullValue() {
+ new Update().min("key", (Date) null);
+ }
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void minShouldBeAppliedCorrectly() {
+
+ Update update = new Update().min("key", 10);
+
+ assertThat(update.getUpdateObject(), equalTo(new BasicDBObjectBuilder().add("$min", new BasicDBObject("key", 10))
+ .get()));
+ }
+
+ /**
+ * @see DATAMONGO-941
+ */
+ @Test
+ public void minShouldBeAppliedToMultipleFieldsCorrectly() {
+
+ Update update = new Update().min("foo", 10).min("bar", 20);
+
+ assertThat(update.getUpdateObject(),
+ equalTo(new BasicDBObjectBuilder().add("$min", new BasicDBObjectBuilder().add("foo", 10).add("bar", 20).get())
+ .get()));
+ }
}