From ce547c022cbfba8e869fac9b90099af2c9d5c732 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 12 Nov 2014 13:51:28 +0100 Subject: [PATCH 1/4] DATAMONGO-941 - Add support for $min to Update. Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-cross-store/pom.xml | 4 ++-- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb-log4j/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 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 From 504f38ab239ada57e8cff998b50f910fd5c987a9 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 13 Nov 2014 15:43:54 +0100 Subject: [PATCH 2/4] DATAMONGO-941 - Add support for Update $min. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now offer dedicated methods for ‘min’ (Number/Date) on Update. As BigInteger and BigDecimal values are converted to String before storing in MongoDB it is not possible to use for the min operation. Therefore we type check both the given value and the property it should be applied to and raise an error if it fails. --- .../mongodb/core/convert/QueryMapper.java | 305 +++++++++++++++++- .../data/mongodb/core/query/Update.java | 33 ++ .../data/mongodb/core/DBObjectTestUtils.java | 52 ++- .../data/mongodb/core/MongoTemplateTests.java | 76 ++++- .../core/convert/UpdateMapperUnitTests.java | 143 +++++++- .../data/mongodb/core/query/UpdateTests.java | 42 +++ 6 files changed, 640 insertions(+), 11 deletions(-) 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..c50c91a452 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 @@ -18,7 +18,10 @@ 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 +30,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 +43,12 @@ 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.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 +103,7 @@ public QueryMapper(MongoConverter converter) { public DBObject getMappedObject(DBObject query, MongoPersistentEntity entity) { if (isNestedKeyword(query)) { - return getMappedKeyword(new Keyword(query), entity); + return getMappedKeyword(KeywordFactory.forDbo(query, entity, mappingContext), entity); } DBObject result = new BasicDBObject(); @@ -192,7 +202,7 @@ protected Entry getMappedObjectForField(Field field, Object rawV Object value; if (isNestedKeyword(rawValue) && !field.isIdField()) { - Keyword keyword = new Keyword((DBObject) rawValue); + Keyword keyword = KeywordFactory.forDbo((DBObject) rawValue, field.getProperty(), mappingContext); value = getMappedKeyword(field, keyword); } else { value = getMappedValue(field, rawValue); @@ -221,6 +231,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 +306,8 @@ protected Object getMappedValue(Field documentField, Object value) { } if (isNestedKeyword(value)) { - return getMappedKeyword(new Keyword((DBObject) value), null); + return getMappedKeyword(KeywordFactory.forDbo((DBObject) value, documentField.getProperty(), mappingContext), + null); } if (isAssociationConversionNecessary(documentField, value)) { @@ -510,17 +523,19 @@ 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 List validators; + private KeywordContext context = new KeywordContext(); private final String key; - private final Object value; public Keyword(DBObject source, String key) { this.key = key; - this.value = source.get(key); + this.context.setValue(source.get(key)); } public Keyword(DBObject dbObject) { @@ -529,7 +544,7 @@ public Keyword(DBObject dbObject) { Assert.isTrue(keys.size() == 1, "Can only use a single value DBObject!"); this.key = keys.iterator().next(); - this.value = dbObject.get(key); + this.context.setValue(dbObject.get(key)); } /** @@ -546,7 +561,7 @@ public boolean isOrOrNor() { } public boolean hasIterableValue() { - return value instanceof Iterable; + return context.isIterableValue(); } public String getKey() { @@ -555,7 +570,281 @@ public String getKey() { @SuppressWarnings("unchecked") public T getValue() { - return (T) value; + return (T) context.getValue(); + } + + /** + * 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(KeywordContext.class)) { + validator.validate(context, validationResult); + } + if (context.getValue() != null && validator.supports(context.getValue().getClass())) { + validator.validate(context, validationResult); + } + } + + if (validationResult.hasErrors()) { + StringBuilder sb = new StringBuilder(); + for (ObjectError error : validationResult.getAllErrors()) { + sb.append(error.getDefaultMessage() + "\r\n"); + } + throw new InvalidDataAccessApiUsageException(sb.toString()); + } + } + + public void registerValidator(Validator validator) { + + if (this.validators == null) { + this.validators = new ArrayList(1); + } + this.validators.add(validator); + } + } + + /** + * Wrapper to simplify usage of {@link Validator}s. + * + * @author Christoph Strobl + * @since 1.7 + */ + static class KeywordContext { + + private MappingContext, MongoPersistentProperty> mappingContext; + private MongoPersistentEntity entity; + private MongoPersistentProperty property; + private Object value; + + boolean isIterableValue() { + return value instanceof Iterable; + } + + public MongoPersistentEntity getEntity() { + return entity; + } + + void setEntity(MongoPersistentEntity entity) { + this.entity = entity; + } + + public MongoPersistentProperty getProperty() { + return property; + } + + void setProperty(MongoPersistentProperty property) { + this.property = property; + } + + public Object getValue() { + return value; + } + + void setValue(Object value) { + this.value = value; + } + + boolean isDBObjectValue() { + return value instanceof DBObject; + } + + public MappingContext, MongoPersistentProperty> getMappingContext() { + return mappingContext; + } + + public void setMappingContext( + MappingContext, MongoPersistentProperty> mappingContext) { + this.mappingContext = mappingContext; + } + + } + + /** + * Creates {@link Keyword} and sets the {@link KeywordContext}. Also registers {@link Validator}s for specific + * keywords. + * + * @author Christoph Strobl + * @since 1.7 + */ + static class KeywordFactory { + + static Keyword forDbo(DBObject source, + MappingContext, MongoPersistentProperty> mappingContext) { + return forDbo(source, (MongoPersistentEntity) null, mappingContext); + } + + static Keyword forDbo(DBObject source, MongoPersistentEntity entity, + MappingContext, MongoPersistentProperty> mappingContext) { + + Keyword keyword = new Keyword(source); + keyword.context.setEntity(entity); + keyword.context.setMappingContext(mappingContext); + registerValidator(keyword); + return keyword; + } + + static Keyword forDbo(DBObject source, MongoPersistentProperty property, + MappingContext, MongoPersistentProperty> mappingContext) { + + Keyword keyword = new Keyword(source); + keyword.context.setProperty(property); + keyword.context.setMappingContext(mappingContext); + if (property != null) { + keyword.context.setEntity((MongoPersistentEntity) property.getOwner()); + } + registerValidator(keyword); + return keyword; + } + + private static void registerValidator(Keyword keyword) { + + if (keyword.getKey().equalsIgnoreCase("$min")) { + keyword.registerValidator(KeywordContextParameterTypeValidator.whitelist(Byte.class, Short.class, + Integer.class, Long.class, Float.class, Double.class, Date.class)); + } + } + } + + /** + * @author Christoph Strobl + * @since 1.7 + */ + static abstract class KeywordValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return ClassUtils.isAssignable(KeywordContext.class, clazz); + } + + public void validate(Object target, Errors errors) { + validate((KeywordContext) target, errors); + } + + public abstract void validate(KeywordContext context, 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 KeywordContextParameterTypeValidator extends KeywordValidator { + + Set> types; + boolean invert = false; + + private KeywordContextParameterTypeValidator(Class... supportedTypes) { + this.types = new HashSet>(Arrays.asList(supportedTypes)); + } + + public static KeywordContextParameterTypeValidator whitelist(Class... supportedTypes) { + return new KeywordContextParameterTypeValidator(supportedTypes); + } + + public static KeywordContextParameterTypeValidator blacklist(Class... unsupportedTypes) { + + KeywordContextParameterTypeValidator validator = new KeywordContextParameterTypeValidator(unsupportedTypes); + validator.invert = true; + return validator; + } + + 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(); + + if (types.contains(ClassUtils.resolvePrimitiveIfNecessary(typeToValidate))) { + if (invert) { + errors.reject("", String.format("Using %s is not supported for %s.", typeToValidate, errors.getObjectName())); + } + } else { + if (!invert) { + 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(KeywordContext target, Errors errors) { + + if (types.isEmpty() || target == null) { + return; + } + + if (target.isDBObjectValue()) { + + DBObject dbo = (DBObject) target.getValue(); + + Iterator keysIterator = dbo.toMap().keySet().iterator(); + while (keysIterator.hasNext()) { + + String propertyName = keysIterator.next().toString(); + String[] parts = propertyName.split("\\."); + if (parts.length == 1) { + doValidate(target.getEntity().getPersistentProperty(propertyName), errors); + } else { + + MongoPersistentEntity propertyScope = target.getEntity(); + + Iterator partsIterator = Arrays.asList(parts).iterator(); + while (partsIterator.hasNext()) { + + MongoPersistentProperty property = (MongoPersistentProperty) propertyScope + .getPersistentProperty(partsIterator.next()); + + if (!partsIterator.hasNext()) { + doValidate(property, errors); + } else if (property != null && property.isEntity() && partsIterator.hasNext()) { + + propertyScope = target.getMappingContext().getPersistentEntity(property.getActualType()); + if (propertyScope == null) { + break; + } + } + } + } + } + } else { + + MongoPersistentProperty propertyToUse = target.getProperty(); + + if (propertyToUse == null && target.getEntity() != null) { + propertyToUse = target.getEntity().getPersistentProperty(errors.getObjectName()); + } + doValidate(propertyToUse, errors); + } } } 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())); + } } From 415e8c278a9036db1bc85bda3047b7ee4055a7de Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Mon, 24 Nov 2014 19:45:36 +0100 Subject: [PATCH 3/4] DATAMONGO-941 - Add support for Update $min. Moved value field from KeywordContext into Keyword directly since it was always accessed directly. Made KeywordContext immutable. Validation now works with Keywords directly instead of using the KeywordContext. Introduced static factory method keywordFor(...) for constructing keywords. --- .../mongodb/core/convert/QueryMapper.java | 238 +++++++++--------- 1 file changed, 116 insertions(+), 122 deletions(-) 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 c50c91a452..0c5ac9c509 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,6 +15,8 @@ */ 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; @@ -45,6 +47,7 @@ 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; @@ -103,7 +106,7 @@ public QueryMapper(MongoConverter converter) { public DBObject getMappedObject(DBObject query, MongoPersistentEntity entity) { if (isNestedKeyword(query)) { - return getMappedKeyword(KeywordFactory.forDbo(query, entity, mappingContext), entity); + return getMappedKeyword(keywordFor(query, entity, mappingContext), entity); } DBObject result = new BasicDBObject(); @@ -121,7 +124,8 @@ public DBObject getMappedObject(DBObject query, MongoPersistentEntity entity) } if (isKeyword(key)) { - result.putAll(getMappedKeyword(new Keyword(query, key), entity)); + result.putAll(getMappedKeyword(new Keyword(query, key, new KeywordContext(null, entity, mappingContext)), + entity)); continue; } @@ -202,7 +206,7 @@ protected Entry getMappedObjectForField(Field field, Object rawV Object value; if (isNestedKeyword(rawValue) && !field.isIdField()) { - Keyword keyword = KeywordFactory.forDbo((DBObject) rawValue, field.getProperty(), mappingContext); + Keyword keyword = keywordFor((DBObject) rawValue, field.getProperty(), mappingContext); value = getMappedKeyword(field, keyword); } else { value = getMappedValue(field, rawValue); @@ -306,8 +310,7 @@ protected Object getMappedValue(Field documentField, Object value) { } if (isNestedKeyword(value)) { - return getMappedKeyword(KeywordFactory.forDbo((DBObject) value, documentField.getProperty(), mappingContext), - null); + return getMappedKeyword(keywordFor((DBObject) value, documentField.getProperty(), mappingContext), null); } if (isAssociationConversionNecessary(documentField, value)) { @@ -529,22 +532,40 @@ static class Keyword { private static final String N_OR_PATTERN = "\\$.*or"; private List validators; - private KeywordContext context = new KeywordContext(); - private final String key; + private final Object value; + private final KeywordContext context; + + private static final Validator MIN_OPERATOR_VALIDATOR = KeywordParameterTypeValidator.whitelist(Byte.class, + Short.class, Integer.class, Long.class, Float.class, Double.class, Date.class); + + public Keyword(DBObject dbo, KeywordContext context) { + this(dbo, assertAndReturnOnlySingleKey(dbo), context); + } + + public Keyword(DBObject dbo, String key, KeywordContext context) { - public Keyword(DBObject source, String key) { this.key = key; - this.context.setValue(source.get(key)); + this.value = dbo.get(key); + this.context = context; + this.validators = createValidators(key); + } + + private List createValidators(String key) { + + if (key.equalsIgnoreCase("$min")) { + return Arrays.asList(MIN_OPERATOR_VALIDATOR); + } + + return Collections. emptyList(); } - public Keyword(DBObject dbObject) { + static String assertAndReturnOnlySingleKey(DBObject dbo) { - Set keys = dbObject.keySet(); + Set keys = dbo.keySet(); Assert.isTrue(keys.size() == 1, "Can only use a single value DBObject!"); - this.key = keys.iterator().next(); - this.context.setValue(dbObject.get(key)); + return keys.iterator().next(); } /** @@ -561,7 +582,11 @@ public boolean isOrOrNor() { } public boolean hasIterableValue() { - return context.isIterableValue(); + return value instanceof Iterable; + } + + boolean isDBObjectValue() { + return value instanceof DBObject; } public String getKey() { @@ -570,7 +595,11 @@ public String getKey() { @SuppressWarnings("unchecked") public T getValue() { - return (T) context.getValue(); + return (T) this.value; + } + + public KeywordContext getContext() { + return context; } /** @@ -587,29 +616,30 @@ public void validate() { MapBindingResult validationResult = new MapBindingResult(new LinkedHashMap(), this.key); for (Validator validator : validators) { - if (validator.supports(KeywordContext.class)) { - validator.validate(context, validationResult); + + if (validator.supports(Keyword.class)) { + validator.validate(this, validationResult); } - if (context.getValue() != null && validator.supports(context.getValue().getClass())) { - validator.validate(context, validationResult); + + if (this.value == null) { + continue; } - } - if (validationResult.hasErrors()) { - StringBuilder sb = new StringBuilder(); - for (ObjectError error : validationResult.getAllErrors()) { - sb.append(error.getDefaultMessage() + "\r\n"); + if (validator.supports(this.value.getClass())) { + validator.validate(context, validationResult); } - throw new InvalidDataAccessApiUsageException(sb.toString()); } - } - public void registerValidator(Validator validator) { + if (!validationResult.hasErrors()) { + return; + } - if (this.validators == null) { - this.validators = new ArrayList(1); + StringBuilder sb = new StringBuilder(); + for (ObjectError error : validationResult.getAllErrors()) { + sb.append(error.getDefaultMessage() + "\r\n"); } - this.validators.add(validator); + + throw new InvalidDataAccessApiUsageException(sb.toString()); } } @@ -621,52 +651,28 @@ public void registerValidator(Validator validator) { */ static class KeywordContext { - private MappingContext, MongoPersistentProperty> mappingContext; - private MongoPersistentEntity entity; - private MongoPersistentProperty property; - private Object value; + private final MongoPersistentProperty property; + private final MongoPersistentEntity entity; + private final MappingContext, MongoPersistentProperty> mappingContext; - boolean isIterableValue() { - return value instanceof Iterable; + public KeywordContext(MongoPersistentProperty property, MongoPersistentEntity entity, + MappingContext, MongoPersistentProperty> mappingContext) { + this.property = property; + this.entity = entity; + this.mappingContext = mappingContext; } public MongoPersistentEntity getEntity() { return entity; } - void setEntity(MongoPersistentEntity entity) { - this.entity = entity; - } - public MongoPersistentProperty getProperty() { return property; } - void setProperty(MongoPersistentProperty property) { - this.property = property; - } - - public Object getValue() { - return value; - } - - void setValue(Object value) { - this.value = value; - } - - boolean isDBObjectValue() { - return value instanceof DBObject; - } - public MappingContext, MongoPersistentProperty> getMappingContext() { return mappingContext; } - - public void setMappingContext( - MappingContext, MongoPersistentProperty> mappingContext) { - this.mappingContext = mappingContext; - } - } /** @@ -678,40 +684,22 @@ public void setMappingContext( */ static class KeywordFactory { - static Keyword forDbo(DBObject source, + static Keyword keywordFor(DBObject source, MappingContext, MongoPersistentProperty> mappingContext) { - return forDbo(source, (MongoPersistentEntity) null, mappingContext); + return keywordFor(source, (MongoPersistentEntity) null, mappingContext); } - static Keyword forDbo(DBObject source, MongoPersistentEntity entity, + static Keyword keywordFor(DBObject source, MongoPersistentEntity entity, MappingContext, MongoPersistentProperty> mappingContext) { - - Keyword keyword = new Keyword(source); - keyword.context.setEntity(entity); - keyword.context.setMappingContext(mappingContext); - registerValidator(keyword); - return keyword; + return new Keyword(source, new KeywordContext(null, entity, mappingContext)); } - static Keyword forDbo(DBObject source, MongoPersistentProperty property, + static Keyword keywordFor(DBObject source, MongoPersistentProperty property, MappingContext, MongoPersistentProperty> mappingContext) { - Keyword keyword = new Keyword(source); - keyword.context.setProperty(property); - keyword.context.setMappingContext(mappingContext); - if (property != null) { - keyword.context.setEntity((MongoPersistentEntity) property.getOwner()); - } - registerValidator(keyword); - return keyword; - } - - private static void registerValidator(Keyword keyword) { - - if (keyword.getKey().equalsIgnoreCase("$min")) { - keyword.registerValidator(KeywordContextParameterTypeValidator.whitelist(Byte.class, Short.class, - Integer.class, Long.class, Float.class, Double.class, Date.class)); - } + KeywordContext context = new KeywordContext(property, property == null ? null + : (MongoPersistentEntity) property.getOwner(), mappingContext); + return new Keyword(source, context); } } @@ -723,14 +711,14 @@ static abstract class KeywordValidator implements Validator { @Override public boolean supports(Class clazz) { - return ClassUtils.isAssignable(KeywordContext.class, clazz); + return ClassUtils.isAssignable(Keyword.class, clazz); } public void validate(Object target, Errors errors) { - validate((KeywordContext) target, errors); + validate((Keyword) target, errors); } - public abstract void validate(KeywordContext context, Errors errors); + public abstract void validate(Keyword keyword, Errors errors); } @@ -742,24 +730,23 @@ public void validate(Object target, Errors errors) { * @author Christoph Strobl * @since 1.7 */ - static class KeywordContextParameterTypeValidator extends KeywordValidator { + static class KeywordParameterTypeValidator extends KeywordValidator { - Set> types; - boolean invert = false; + private final Set> types; + private final boolean invert; - private KeywordContextParameterTypeValidator(Class... supportedTypes) { + private KeywordParameterTypeValidator(boolean invert, Class... supportedTypes) { + + this.invert = invert; this.types = new HashSet>(Arrays.asList(supportedTypes)); } - public static KeywordContextParameterTypeValidator whitelist(Class... supportedTypes) { - return new KeywordContextParameterTypeValidator(supportedTypes); + public static KeywordParameterTypeValidator whitelist(Class... supportedTypes) { + return new KeywordParameterTypeValidator(false, supportedTypes); } - public static KeywordContextParameterTypeValidator blacklist(Class... unsupportedTypes) { - - KeywordContextParameterTypeValidator validator = new KeywordContextParameterTypeValidator(unsupportedTypes); - validator.invert = true; - return validator; + public static KeywordParameterTypeValidator blacklist(Class... unsupportedTypes) { + return new KeywordParameterTypeValidator(true, unsupportedTypes); } private void doValidate(Object candidate, Errors errors) { @@ -775,6 +762,7 @@ private void doValidate(Object candidate, Errors errors) { while (it.hasNext()) { doValidate(value.get(it.next().toString()), errors); } + return; } @@ -797,7 +785,7 @@ private void doValidate(Object candidate, Errors errors) { * @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(KeywordContext target, Errors errors) { + public void validate(Keyword target, Errors errors) { if (types.isEmpty() || target == null) { return; @@ -811,38 +799,44 @@ public void validate(KeywordContext target, Errors errors) { while (keysIterator.hasNext()) { String propertyName = keysIterator.next().toString(); - String[] parts = propertyName.split("\\."); - if (parts.length == 1) { - doValidate(target.getEntity().getPersistentProperty(propertyName), errors); - } else { - MongoPersistentEntity propertyScope = target.getEntity(); + if (StringUtils.countOccurrencesOf(propertyName, ".") == 0) { + doValidate(target.getContext().getEntity().getPersistentProperty(propertyName), errors); + continue; + } - Iterator partsIterator = Arrays.asList(parts).iterator(); - while (partsIterator.hasNext()) { + MongoPersistentEntity propertyScope = target.getContext().getEntity(); - MongoPersistentProperty property = (MongoPersistentProperty) propertyScope - .getPersistentProperty(partsIterator.next()); + Iterator partsIterator = Arrays.asList(propertyName.split("\\.")).iterator(); + while (partsIterator.hasNext()) { - if (!partsIterator.hasNext()) { - doValidate(property, errors); - } else if (property != null && property.isEntity() && partsIterator.hasNext()) { + MongoPersistentProperty property = (MongoPersistentProperty) propertyScope + .getPersistentProperty(partsIterator.next()); + + if (!partsIterator.hasNext()) { + doValidate(property, errors); + continue; + } - propertyScope = target.getMappingContext().getPersistentEntity(property.getActualType()); - if (propertyScope == null) { - break; - } + if (property != null && property.isEntity() && partsIterator.hasNext()) { + + propertyScope = target.getContext().getMappingContext().getPersistentEntity(property.getActualType()); + + if (propertyScope == null) { + break; } } } + } } else { - MongoPersistentProperty propertyToUse = target.getProperty(); + MongoPersistentProperty propertyToUse = target.getContext().getProperty(); - if (propertyToUse == null && target.getEntity() != null) { - propertyToUse = target.getEntity().getPersistentProperty(errors.getObjectName()); + if (propertyToUse == null && target.getContext().getEntity() != null) { + propertyToUse = target.getContext().getEntity().getPersistentProperty(errors.getObjectName()); } + doValidate(propertyToUse, errors); } } From de22f2d158afba3cd26ce2c3580a8668a1bb87d0 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Tue, 25 Nov 2014 10:59:34 +0100 Subject: [PATCH 4/4] DATAMONGO-941 - Add support for Update $min. Moved construction of validators to KeywordFactory. Reduced nesting in Keyword validation. --- .../mongodb/core/convert/QueryMapper.java | 175 ++++++++++-------- 1 file changed, 93 insertions(+), 82 deletions(-) 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 0c5ac9c509..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 @@ -106,7 +106,7 @@ public QueryMapper(MongoConverter converter) { public DBObject getMappedObject(DBObject query, MongoPersistentEntity entity) { if (isNestedKeyword(query)) { - return getMappedKeyword(keywordFor(query, entity, mappingContext), entity); + return getMappedKeyword(keywordFor(query, entity, null, mappingContext), entity); } DBObject result = new BasicDBObject(); @@ -124,8 +124,7 @@ public DBObject getMappedObject(DBObject query, MongoPersistentEntity entity) } if (isKeyword(key)) { - result.putAll(getMappedKeyword(new Keyword(query, key, new KeywordContext(null, entity, mappingContext)), - entity)); + result.putAll(getMappedKeyword(keywordFor(key, query.get(key), entity, null, mappingContext), entity)); continue; } @@ -531,41 +530,17 @@ protected boolean isKeyword(String candidate) { static class Keyword { private static final String N_OR_PATTERN = "\\$.*or"; - private List validators; private final String key; private final Object value; private final KeywordContext context; + private final List validators; - private static final Validator MIN_OPERATOR_VALIDATOR = KeywordParameterTypeValidator.whitelist(Byte.class, - Short.class, Integer.class, Long.class, Float.class, Double.class, Date.class); - - public Keyword(DBObject dbo, KeywordContext context) { - this(dbo, assertAndReturnOnlySingleKey(dbo), context); - } - - public Keyword(DBObject dbo, String key, KeywordContext context) { + public Keyword(String key, Object value, KeywordContext context, List validators) { this.key = key; - this.value = dbo.get(key); + this.value = value; this.context = context; - this.validators = createValidators(key); - } - - private List createValidators(String key) { - - if (key.equalsIgnoreCase("$min")) { - return Arrays.asList(MIN_OPERATOR_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(); + this.validators = validators; } /** @@ -682,24 +657,55 @@ public MappingContext, MongoPersistentPropert * @author Christoph Strobl * @since 1.7 */ - static class KeywordFactory { + 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, mappingContext); + return keywordFor(source, (MongoPersistentEntity) null, null, mappingContext); } - static Keyword keywordFor(DBObject source, MongoPersistentEntity entity, + static Keyword keywordFor(DBObject source, MongoPersistentProperty property, MappingContext, MongoPersistentProperty> mappingContext) { - return new Keyword(source, new KeywordContext(null, entity, mappingContext)); + return keywordFor(source, null, property, mappingContext); } - static Keyword keywordFor(DBObject source, MongoPersistentProperty property, + static Keyword keywordFor(DBObject source, MongoPersistentEntity entity, MongoPersistentProperty property, MappingContext, MongoPersistentProperty> mappingContext) { - KeywordContext context = new KeywordContext(property, property == null ? null + 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(source, context); + + 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(); } } @@ -732,21 +738,25 @@ public void validate(Object target, Errors errors) { */ static class KeywordParameterTypeValidator extends KeywordValidator { + enum Mode { + WHITE_LIST, BLACK_LIST; + } + private final Set> types; - private final boolean invert; + private final Mode mode; - private KeywordParameterTypeValidator(boolean invert, Class... supportedTypes) { + private KeywordParameterTypeValidator(Mode mode, Class... supportedTypes) { - this.invert = invert; + this.mode = mode; this.types = new HashSet>(Arrays.asList(supportedTypes)); } public static KeywordParameterTypeValidator whitelist(Class... supportedTypes) { - return new KeywordParameterTypeValidator(false, supportedTypes); + return new KeywordParameterTypeValidator(Mode.WHITE_LIST, supportedTypes); } public static KeywordParameterTypeValidator blacklist(Class... unsupportedTypes) { - return new KeywordParameterTypeValidator(true, unsupportedTypes); + return new KeywordParameterTypeValidator(Mode.BLACK_LIST, unsupportedTypes); } private void doValidate(Object candidate, Errors errors) { @@ -768,15 +778,11 @@ private void doValidate(Object candidate, Errors errors) { Class typeToValidate = ClassUtils.isAssignable(MongoPersistentProperty.class, candidate.getClass()) ? ((MongoPersistentProperty) candidate) .getActualType() : candidate.getClass(); + boolean givenTypeContainedInTypes = types.contains(ClassUtils.resolvePrimitiveIfNecessary(typeToValidate)); - if (types.contains(ClassUtils.resolvePrimitiveIfNecessary(typeToValidate))) { - if (invert) { - errors.reject("", String.format("Using %s is not supported for %s.", typeToValidate, errors.getObjectName())); - } - } else { - if (!invert) { - errors.reject("", String.format("Using %s is not supported for %s.", typeToValidate, errors.getObjectName())); - } + 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())); } } @@ -791,53 +797,58 @@ public void validate(Keyword target, Errors errors) { return; } - if (target.isDBObjectValue()) { + if (!target.isDBObjectValue()) { - DBObject dbo = (DBObject) target.getValue(); + MongoPersistentProperty propertyToUse = target.getContext().getProperty(); - Iterator keysIterator = dbo.toMap().keySet().iterator(); - while (keysIterator.hasNext()) { + if (propertyToUse == null && target.getContext().getEntity() != null) { + propertyToUse = target.getContext().getEntity().getPersistentProperty(errors.getObjectName()); + } - String propertyName = keysIterator.next().toString(); + doValidate(propertyToUse, errors); + return; + } - if (StringUtils.countOccurrencesOf(propertyName, ".") == 0) { - doValidate(target.getContext().getEntity().getPersistentProperty(propertyName), errors); - continue; - } + DBObject dbo = (DBObject) target.getValue(); - MongoPersistentEntity propertyScope = target.getContext().getEntity(); + Iterator keysIterator = dbo.keySet().iterator(); + while (keysIterator.hasNext()) { + validatePropertyPath(keysIterator.next().toString(), target.getContext(), errors); + } + } - Iterator partsIterator = Arrays.asList(propertyName.split("\\.")).iterator(); - while (partsIterator.hasNext()) { + private void validatePropertyPath(String propertyPath, KeywordContext context, Errors errors) { - MongoPersistentProperty property = (MongoPersistentProperty) propertyScope - .getPersistentProperty(partsIterator.next()); + if (StringUtils.countOccurrencesOf(propertyPath, ".") == 0) { + doValidate(context.getEntity().getPersistentProperty(propertyPath), errors); + return; + } - if (!partsIterator.hasNext()) { - doValidate(property, errors); - continue; - } + MongoPersistentEntity persistentEntity = context.getEntity(); - if (property != null && property.isEntity() && partsIterator.hasNext()) { + Iterator partsIterator = Arrays.asList(propertyPath.split("\\.")).iterator(); + while (partsIterator.hasNext()) { - propertyScope = target.getContext().getMappingContext().getPersistentEntity(property.getActualType()); + MongoPersistentProperty persistentProperty = (MongoPersistentProperty) persistentEntity + .getPersistentProperty(partsIterator.next()); - if (propertyScope == null) { - break; - } - } - } + if (persistentProperty == null) { + continue; + } + if (!partsIterator.hasNext()) { + doValidate(persistentProperty, errors); + return; } - } else { - MongoPersistentProperty propertyToUse = target.getContext().getProperty(); + if (persistentProperty.isEntity() && partsIterator.hasNext()) { - if (propertyToUse == null && target.getContext().getEntity() != null) { - propertyToUse = target.getContext().getEntity().getPersistentProperty(errors.getObjectName()); - } + persistentEntity = context.getMappingContext().getPersistentEntity(persistentProperty.getActualType()); - doValidate(propertyToUse, errors); + if (persistentEntity == null) { + return; + } + } } } }