18
18
import java .util .ArrayList ;
19
19
import java .util .Arrays ;
20
20
import java .util .Collections ;
21
+ import java .util .Date ;
22
+ import java .util .HashSet ;
21
23
import java .util .Iterator ;
24
+ import java .util .LinkedHashMap ;
22
25
import java .util .List ;
23
26
import java .util .Map .Entry ;
24
27
import java .util .Set ;
27
30
import org .springframework .core .convert .ConversionException ;
28
31
import org .springframework .core .convert .ConversionService ;
29
32
import org .springframework .core .convert .converter .Converter ;
33
+ import org .springframework .dao .InvalidDataAccessApiUsageException ;
30
34
import org .springframework .data .mapping .Association ;
31
35
import org .springframework .data .mapping .PersistentEntity ;
32
36
import org .springframework .data .mapping .PropertyPath ;
39
43
import org .springframework .data .mongodb .core .mapping .MongoPersistentProperty .PropertyToFieldNameConverter ;
40
44
import org .springframework .data .mongodb .core .query .Query ;
41
45
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 ;
42
52
43
53
import com .mongodb .BasicDBList ;
44
54
import com .mongodb .BasicDBObject ;
@@ -93,7 +103,7 @@ public QueryMapper(MongoConverter converter) {
93
103
public DBObject getMappedObject (DBObject query , MongoPersistentEntity <?> entity ) {
94
104
95
105
if (isNestedKeyword (query )) {
96
- return getMappedKeyword (new Keyword (query ), entity );
106
+ return getMappedKeyword (KeywordFactory . forDbo (query , entity , mappingContext ), entity );
97
107
}
98
108
99
109
DBObject result = new BasicDBObject ();
@@ -192,7 +202,7 @@ protected Entry<String, Object> getMappedObjectForField(Field field, Object rawV
192
202
Object value ;
193
203
194
204
if (isNestedKeyword (rawValue ) && !field .isIdField ()) {
195
- Keyword keyword = new Keyword ((DBObject ) rawValue );
205
+ Keyword keyword = KeywordFactory . forDbo ((DBObject ) rawValue , field . getProperty (), mappingContext );
196
206
value = getMappedKeyword (field , keyword );
197
207
} else {
198
208
value = getMappedValue (field , rawValue );
@@ -221,6 +231,8 @@ protected Field createPropertyField(MongoPersistentEntity<?> entity, String key,
221
231
*/
222
232
protected DBObject getMappedKeyword (Keyword keyword , MongoPersistentEntity <?> entity ) {
223
233
234
+ keyword .validate ();
235
+
224
236
// $or/$nor
225
237
if (keyword .isOrOrNor () || keyword .hasIterableValue ()) {
226
238
@@ -294,7 +306,8 @@ protected Object getMappedValue(Field documentField, Object value) {
294
306
}
295
307
296
308
if (isNestedKeyword (value )) {
297
- return getMappedKeyword (new Keyword ((DBObject ) value ), null );
309
+ return getMappedKeyword (KeywordFactory .forDbo ((DBObject ) value , documentField .getProperty (), mappingContext ),
310
+ null );
298
311
}
299
312
300
313
if (isAssociationConversionNecessary (documentField , value )) {
@@ -510,17 +523,19 @@ protected boolean isKeyword(String candidate) {
510
523
* Value object to capture a query keyword representation.
511
524
*
512
525
* @author Oliver Gierke
526
+ * @author Christoph Strobl
513
527
*/
514
528
static class Keyword {
515
529
516
530
private static final String N_OR_PATTERN = "\\ $.*or" ;
531
+ private List <Validator > validators ;
532
+ private KeywordContext context = new KeywordContext ();
517
533
518
534
private final String key ;
519
- private final Object value ;
520
535
521
536
public Keyword (DBObject source , String key ) {
522
537
this .key = key ;
523
- this .value = source .get (key );
538
+ this .context . setValue ( source .get (key ) );
524
539
}
525
540
526
541
public Keyword (DBObject dbObject ) {
@@ -529,7 +544,7 @@ public Keyword(DBObject dbObject) {
529
544
Assert .isTrue (keys .size () == 1 , "Can only use a single value DBObject!" );
530
545
531
546
this .key = keys .iterator ().next ();
532
- this .value = dbObject .get (key );
547
+ this .context . setValue ( dbObject .get (key ) );
533
548
}
534
549
535
550
/**
@@ -546,7 +561,7 @@ public boolean isOrOrNor() {
546
561
}
547
562
548
563
public boolean hasIterableValue () {
549
- return value instanceof Iterable ;
564
+ return context . isIterableValue () ;
550
565
}
551
566
552
567
public String getKey () {
@@ -555,7 +570,281 @@ public String getKey() {
555
570
556
571
@ SuppressWarnings ("unchecked" )
557
572
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
+ }
559
848
}
560
849
}
561
850
0 commit comments