Skip to content

Commit 9ce02dc

Browse files
DATAMONGO-941 - Add support for Update $min.
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.
1 parent 4a41727 commit 9ce02dc

File tree

6 files changed

+627
-10
lines changed

6 files changed

+627
-10
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

Lines changed: 297 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
import java.util.ArrayList;
1919
import java.util.Arrays;
2020
import java.util.Collections;
21+
import java.util.Date;
22+
import java.util.HashSet;
2123
import java.util.Iterator;
24+
import java.util.LinkedHashMap;
2225
import java.util.List;
2326
import java.util.Map.Entry;
2427
import java.util.Set;
@@ -27,6 +30,7 @@
2730
import org.springframework.core.convert.ConversionException;
2831
import org.springframework.core.convert.ConversionService;
2932
import org.springframework.core.convert.converter.Converter;
33+
import org.springframework.dao.InvalidDataAccessApiUsageException;
3034
import org.springframework.data.mapping.Association;
3135
import org.springframework.data.mapping.PersistentEntity;
3236
import org.springframework.data.mapping.PropertyPath;
@@ -39,6 +43,12 @@
3943
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty.PropertyToFieldNameConverter;
4044
import org.springframework.data.mongodb.core.query.Query;
4145
import org.springframework.util.Assert;
46+
import org.springframework.util.ClassUtils;
47+
import org.springframework.util.CollectionUtils;
48+
import org.springframework.validation.Errors;
49+
import org.springframework.validation.MapBindingResult;
50+
import org.springframework.validation.ObjectError;
51+
import org.springframework.validation.Validator;
4252

4353
import com.mongodb.BasicDBList;
4454
import com.mongodb.BasicDBObject;
@@ -93,7 +103,7 @@ public QueryMapper(MongoConverter converter) {
93103
public DBObject getMappedObject(DBObject query, MongoPersistentEntity<?> entity) {
94104

95105
if (isNestedKeyword(query)) {
96-
return getMappedKeyword(new Keyword(query), entity);
106+
return getMappedKeyword(KeywordFactory.forDbo(query, entity, mappingContext), entity);
97107
}
98108

99109
DBObject result = new BasicDBObject();
@@ -192,7 +202,7 @@ protected Entry<String, Object> getMappedObjectForField(Field field, Object rawV
192202
Object value;
193203

194204
if (isNestedKeyword(rawValue) && !field.isIdField()) {
195-
Keyword keyword = new Keyword((DBObject) rawValue);
205+
Keyword keyword = KeywordFactory.forDbo((DBObject) rawValue, field.getProperty(), mappingContext);
196206
value = getMappedKeyword(field, keyword);
197207
} else {
198208
value = getMappedValue(field, rawValue);
@@ -221,6 +231,8 @@ protected Field createPropertyField(MongoPersistentEntity<?> entity, String key,
221231
*/
222232
protected DBObject getMappedKeyword(Keyword keyword, MongoPersistentEntity<?> entity) {
223233

234+
keyword.validate();
235+
224236
// $or/$nor
225237
if (keyword.isOrOrNor() || keyword.hasIterableValue()) {
226238

@@ -294,7 +306,8 @@ protected Object getMappedValue(Field documentField, Object value) {
294306
}
295307

296308
if (isNestedKeyword(value)) {
297-
return getMappedKeyword(new Keyword((DBObject) value), null);
309+
return getMappedKeyword(KeywordFactory.forDbo((DBObject) value, documentField.getProperty(), mappingContext),
310+
null);
298311
}
299312

300313
if (isAssociationConversionNecessary(documentField, value)) {
@@ -510,17 +523,19 @@ protected boolean isKeyword(String candidate) {
510523
* Value object to capture a query keyword representation.
511524
*
512525
* @author Oliver Gierke
526+
* @author Christoph Strobl
513527
*/
514528
static class Keyword {
515529

516530
private static final String N_OR_PATTERN = "\\$.*or";
531+
private List<Validator> validators;
532+
private KeywordContext context = new KeywordContext();
517533

518534
private final String key;
519-
private final Object value;
520535

521536
public Keyword(DBObject source, String key) {
522537
this.key = key;
523-
this.value = source.get(key);
538+
this.context.setValue(source.get(key));
524539
}
525540

526541
public Keyword(DBObject dbObject) {
@@ -529,7 +544,7 @@ public Keyword(DBObject dbObject) {
529544
Assert.isTrue(keys.size() == 1, "Can only use a single value DBObject!");
530545

531546
this.key = keys.iterator().next();
532-
this.value = dbObject.get(key);
547+
this.context.setValue(dbObject.get(key));
533548
}
534549

535550
/**
@@ -546,7 +561,7 @@ public boolean isOrOrNor() {
546561
}
547562

548563
public boolean hasIterableValue() {
549-
return value instanceof Iterable;
564+
return context.isIterableValue();
550565
}
551566

552567
public String getKey() {
@@ -555,7 +570,281 @@ public String getKey() {
555570

556571
@SuppressWarnings("unchecked")
557572
public <T> T getValue() {
558-
return (T) value;
573+
return (T) context.getValue();
574+
}
575+
576+
/**
577+
* Validate the keyword within the boundary of its {@link KeywordContext}.
578+
*
579+
* @since 1.7
580+
*/
581+
@SuppressWarnings("rawtypes")
582+
public void validate() {
583+
584+
if (CollectionUtils.isEmpty(validators)) {
585+
return;
586+
}
587+
588+
MapBindingResult validationResult = new MapBindingResult(new LinkedHashMap(), this.key);
589+
for (Validator validator : validators) {
590+
if (validator.supports(KeywordContext.class)) {
591+
validator.validate(context, validationResult);
592+
}
593+
if (context.getValue() != null && validator.supports(context.getValue().getClass())) {
594+
validator.validate(context, validationResult);
595+
}
596+
}
597+
598+
if (validationResult.hasErrors()) {
599+
StringBuilder sb = new StringBuilder();
600+
for (ObjectError error : validationResult.getAllErrors()) {
601+
sb.append(error.getDefaultMessage() + "\r\n");
602+
}
603+
throw new InvalidDataAccessApiUsageException(sb.toString());
604+
}
605+
}
606+
607+
public void registerValidator(Validator validator) {
608+
609+
if (this.validators == null) {
610+
this.validators = new ArrayList<Validator>(1);
611+
}
612+
this.validators.add(validator);
613+
}
614+
}
615+
616+
/**
617+
* Wrapper to simplify usage of {@link Validator}s.
618+
*
619+
* @author Christoph Strobl
620+
* @since 1.7
621+
*/
622+
static class KeywordContext {
623+
624+
private MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
625+
private MongoPersistentEntity<?> entity;
626+
private MongoPersistentProperty property;
627+
private Object value;
628+
629+
boolean isIterableValue() {
630+
return value instanceof Iterable;
631+
}
632+
633+
public MongoPersistentEntity<?> getEntity() {
634+
return entity;
635+
}
636+
637+
void setEntity(MongoPersistentEntity<?> entity) {
638+
this.entity = entity;
639+
}
640+
641+
public MongoPersistentProperty getProperty() {
642+
return property;
643+
}
644+
645+
void setProperty(MongoPersistentProperty property) {
646+
this.property = property;
647+
}
648+
649+
public Object getValue() {
650+
return value;
651+
}
652+
653+
void setValue(Object value) {
654+
this.value = value;
655+
}
656+
657+
boolean isDBObjectValue() {
658+
return value instanceof DBObject;
659+
}
660+
661+
public MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> getMappingContext() {
662+
return mappingContext;
663+
}
664+
665+
public void setMappingContext(
666+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
667+
this.mappingContext = mappingContext;
668+
}
669+
670+
}
671+
672+
/**
673+
* Creates {@link Keyword} and sets the {@link KeywordContext}. Also registers {@link Validator}s for specific
674+
* keywords.
675+
*
676+
* @author Christoph Strobl
677+
* @since 1.7
678+
*/
679+
static class KeywordFactory {
680+
681+
static Keyword forDbo(DBObject source,
682+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
683+
return forDbo(source, (MongoPersistentEntity<?>) null, mappingContext);
684+
}
685+
686+
static Keyword forDbo(DBObject source, MongoPersistentEntity<?> entity,
687+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
688+
689+
Keyword keyword = new Keyword(source);
690+
keyword.context.setEntity(entity);
691+
keyword.context.setMappingContext(mappingContext);
692+
registerValidator(keyword);
693+
return keyword;
694+
}
695+
696+
static Keyword forDbo(DBObject source, MongoPersistentProperty property,
697+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
698+
699+
Keyword keyword = new Keyword(source);
700+
keyword.context.setProperty(property);
701+
keyword.context.setMappingContext(mappingContext);
702+
if (property != null) {
703+
keyword.context.setEntity((MongoPersistentEntity<?>) property.getOwner());
704+
}
705+
registerValidator(keyword);
706+
return keyword;
707+
}
708+
709+
private static void registerValidator(Keyword keyword) {
710+
711+
if (keyword.getKey().equalsIgnoreCase("$min")) {
712+
keyword.registerValidator(KeywordContextParameterTypeValidator.whitelist(Byte.class, Short.class,
713+
Integer.class, Long.class, Float.class, Double.class, Date.class));
714+
}
715+
}
716+
}
717+
718+
/**
719+
* @author Christoph Strobl
720+
* @since 1.7
721+
*/
722+
static abstract class KeywordValidator implements Validator {
723+
724+
@Override
725+
public boolean supports(Class<?> clazz) {
726+
return ClassUtils.isAssignable(KeywordContext.class, clazz);
727+
}
728+
729+
public void validate(Object target, Errors errors) {
730+
validate((KeywordContext) target, errors);
731+
}
732+
733+
public abstract void validate(KeywordContext context, Errors errors);
734+
735+
}
736+
737+
/**
738+
* {@link Validator} checking type attributes for keyowords on the corresponding value and
739+
* {@link MongoPersistentProperty}. In case the value to check is a {@link DBObject} all nested values will be
740+
* recoursively checked.
741+
*
742+
* @author Christoph Strobl
743+
* @since 1.7
744+
*/
745+
static class KeywordContextParameterTypeValidator extends KeywordValidator {
746+
747+
Set<Class<?>> types;
748+
boolean invert = false;
749+
750+
private KeywordContextParameterTypeValidator(Class<?>... supportedTypes) {
751+
this.types = new HashSet<Class<?>>(Arrays.asList(supportedTypes));
752+
}
753+
754+
public static KeywordContextParameterTypeValidator whitelist(Class<?>... supportedTypes) {
755+
return new KeywordContextParameterTypeValidator(supportedTypes);
756+
}
757+
758+
public static KeywordContextParameterTypeValidator blacklist(Class<?>... unsupportedTypes) {
759+
760+
KeywordContextParameterTypeValidator validator = new KeywordContextParameterTypeValidator(unsupportedTypes);
761+
validator.invert = true;
762+
return validator;
763+
}
764+
765+
private void doValidate(Object candidate, Errors errors) {
766+
767+
if (types.isEmpty() || candidate == null) {
768+
return;
769+
}
770+
771+
if (candidate instanceof DBObject) {
772+
DBObject value = (DBObject) candidate;
773+
774+
Iterator<?> it = value.toMap().keySet().iterator();
775+
while (it.hasNext()) {
776+
doValidate(value.get(it.next().toString()), errors);
777+
}
778+
return;
779+
}
780+
781+
Class<?> typeToValidate = ClassUtils.isAssignable(MongoPersistentProperty.class, candidate.getClass()) ? ((MongoPersistentProperty) candidate)
782+
.getActualType() : candidate.getClass();
783+
784+
if (types.contains(ClassUtils.resolvePrimitiveIfNecessary(typeToValidate))) {
785+
if (invert) {
786+
errors.reject("", String.format("Using %s is not supported for %s.", typeToValidate, errors.getObjectName()));
787+
}
788+
} else {
789+
if (!invert) {
790+
errors.reject("", String.format("Using %s is not supported for %s.", typeToValidate, errors.getObjectName()));
791+
}
792+
}
793+
}
794+
795+
/*
796+
* (non-Javadoc)
797+
* @see org.springframework.data.mongodb.core.convert.QueryMapper.KeywordValidator#validate(org.springframework.data.mongodb.core.convert.QueryMapper.KeywordContext, org.springframework.validation.Errors)
798+
*/
799+
@Override
800+
public void validate(KeywordContext target, Errors errors) {
801+
802+
if (types.isEmpty() || target == null) {
803+
return;
804+
}
805+
806+
if (target.isDBObjectValue()) {
807+
808+
DBObject dbo = (DBObject) target.getValue();
809+
810+
Iterator<?> keysIterator = dbo.toMap().keySet().iterator();
811+
while (keysIterator.hasNext()) {
812+
813+
String propertyName = keysIterator.next().toString();
814+
String[] parts = propertyName.split("\\.");
815+
if (parts.length == 1) {
816+
doValidate(target.getEntity().getPersistentProperty(propertyName), errors);
817+
} else {
818+
819+
MongoPersistentEntity<?> propertyScope = target.getEntity();
820+
821+
Iterator<String> partsIterator = Arrays.asList(parts).iterator();
822+
while (partsIterator.hasNext()) {
823+
824+
MongoPersistentProperty property = (MongoPersistentProperty) propertyScope
825+
.getPersistentProperty(partsIterator.next());
826+
827+
if (!partsIterator.hasNext()) {
828+
doValidate(property, errors);
829+
} else if (property != null && property.isEntity() && partsIterator.hasNext()) {
830+
831+
propertyScope = target.getMappingContext().getPersistentEntity(property.getActualType());
832+
if (propertyScope == null) {
833+
break;
834+
}
835+
}
836+
}
837+
}
838+
}
839+
} else {
840+
841+
MongoPersistentProperty propertyToUse = target.getProperty();
842+
843+
if (propertyToUse == null && target.getEntity() != null) {
844+
propertyToUse = target.getEntity().getPersistentProperty(errors.getObjectName());
845+
}
846+
doValidate(propertyToUse, errors);
847+
}
559848
}
560849
}
561850

0 commit comments

Comments
 (0)