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, MongoPersistentProperty> mappingContext; + + public KeywordContext(MongoPersistentProperty property, MongoPersistentEntity entity, + MappingContext, MongoPersistentProperty> mappingContext) { + this.property = property; + this.entity = entity; + this.mappingContext = mappingContext; + } + + public MongoPersistentEntity getEntity() { + return entity; + } + + public MongoPersistentProperty getProperty() { + return property; + } + + public MappingContext, 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, MongoPersistentProperty> mappingContext) { + return keywordFor(source, (MongoPersistentEntity) null, null, mappingContext); + } + + static Keyword keywordFor(DBObject source, MongoPersistentProperty property, + MappingContext, MongoPersistentProperty> mappingContext) { + return keywordFor(source, null, property, mappingContext); + } + + static Keyword keywordFor(DBObject source, MongoPersistentEntity entity, MongoPersistentProperty property, + MappingContext, 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, 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())); + } }