001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.schematic.internal.schema;
017
018import java.net.URI;
019import java.net.URISyntaxException;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.EnumSet;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Set;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029import java.util.regex.PatternSyntaxException;
030import java.util.stream.Collectors;
031import org.modeshape.schematic.SchemaLibrary;
032import org.modeshape.schematic.document.Document;
033import org.modeshape.schematic.document.Document.Field;
034import org.modeshape.schematic.document.JsonSchema;
035import org.modeshape.schematic.document.JsonSchema.Type;
036import org.modeshape.schematic.document.Null;
037import org.modeshape.schematic.document.Path;
038import org.modeshape.schematic.document.Symbol;
039import org.modeshape.schematic.internal.document.Paths;
040
041public class JsonSchemaValidatorFactory implements Validator.Factory {
042
043    private CompositeValidator topLevelValidator = new CompositeValidator();
044    private final Problems problems;
045    private final URI uri;
046
047    protected JsonSchemaValidatorFactory( URI uri,
048                                          Problems problems ) {
049        this.uri = uri;
050        this.problems = problems;
051    }
052
053    @Override
054    public Validator create( Document schemaDocument,
055                             Path pathToDoc ) {
056        CompositeValidator validators = new CompositeValidator();
057        if (this.topLevelValidator == null) {
058            this.topLevelValidator = validators;
059        }
060
061        // Dereference any "$ref" value, replacing this schema document with the referenced one ...
062        Validator derefValidator = dereference(schemaDocument, pathToDoc, problems);
063        if (derefValidator != null) {
064            return derefValidator;
065        }
066
067        addValidatorsForTypes(schemaDocument, pathToDoc, problems, validators);
068        addValidatorsForProperties(schemaDocument, pathToDoc, problems, validators);
069        addValidatorsForPatternProperties(schemaDocument, pathToDoc, problems, validators);
070        addValidatorsForItems(schemaDocument, pathToDoc, problems, validators);
071        addValidatorsForRequired(schemaDocument, pathToDoc, problems, validators);
072        addValidatorsForMinimum(schemaDocument, pathToDoc, problems, validators);
073        addValidatorsForMaximum(schemaDocument, pathToDoc, problems, validators);
074        addValidatorsForMinimumItems(schemaDocument, pathToDoc, problems, validators);
075        addValidatorsForMaximumItems(schemaDocument, pathToDoc, problems, validators);
076        addValidatorsForUniqueItems(schemaDocument, pathToDoc, problems, validators);
077        addValidatorsForPattern(schemaDocument, pathToDoc, problems, validators);
078        addValidatorsForMinimumLength(schemaDocument, pathToDoc, problems, validators);
079        addValidatorsForMaximumLength(schemaDocument, pathToDoc, problems, validators);
080        addValidatorsForEnum(schemaDocument, pathToDoc, problems, validators);
081        addValidatorsForDivisibleBy(schemaDocument, pathToDoc, problems, validators);
082        addValidatorsForDisallowedTypes(schemaDocument, pathToDoc, problems, validators);
083
084        switch (validators.size()) {
085            case 0:
086                return null;
087            case 1:
088                return validators.getFirst();
089            default:
090                return validators;
091        }
092    }
093
094    protected Validator dereference( Document schemaDocument,
095                                     Path pathToDoc,
096                                     Problems problems ) {
097        String ref = schemaDocument.getString("$ref");
098        if (ref == null) {
099            return null;
100        }
101        if ("#".equals(ref)) {
102            return topLevelValidator;
103        }
104        // Try to resolve the absolute or relative key ...
105        // See if this is a relative URI ...
106        String resolvedReference = null;
107        URI refUri = null;
108        try {
109            refUri = new URI(ref);
110            URI resolvedUri = this.uri.resolve(refUri);
111            resolvedReference = resolvedUri.toString();
112        } catch (URISyntaxException e) {
113            problems.recordWarning(pathToDoc, "The URI of the referenced schema '" + uri + "' is not a valid URI");
114        }
115        if (uri.equals(resolvedReference)) {
116            return topLevelValidator;
117        }
118        if (!ref.equals(resolvedReference)) {
119            // The resolved reference is different than what we just looked up, so look it up ...
120            assert resolvedReference != null;
121            return new ResolvingValidator(resolvedReference);
122        }
123        return null;
124    }
125
126    protected void addValidatorsForTypes( Document parent,
127                                          Path parentPath,
128                                          Problems problems,
129                                          CompositeValidator validators ) {
130        Object value = parent.get("type");
131        if (value instanceof String) {
132            // Simple type ...
133            Type type = JsonSchema.Type.byName((String)value);
134            if (type == Type.ANY || type == Type.UNKNOWN) return;
135            validators.add(new TypeValidator(type));
136        } else if (value instanceof List<?>) {
137            // Union type ...
138            List<Validator> unionValidators = new ArrayList<Validator>();
139            List<?> types = (List<?>)value;
140            for (Object obj : types) {
141                Validator validator = null;
142                if (obj instanceof Document) {
143                    // It's either a schema or a reference to a schema ...
144                    Document schemaOrRef = (Document)obj;
145                    validator = create(schemaOrRef, parentPath.with("type"));
146                } else if (obj instanceof String) {
147                    Type type = JsonSchema.Type.byName((String)obj);
148                    if (type == Type.ANY || type == Type.UNKNOWN) continue;
149                    validator = new TypeValidator(type);
150                }
151                if (validator != null) unionValidators.add(validator);
152            }
153            if (unionValidators.size() == 1) {
154                // Just one validator ...
155                validators.add(unionValidators.get(0));
156            } else if (unionValidators.size() > 1) {
157                // More than one validator, so use a union ...
158                validators.add(new UnionValidator(unionValidators));
159            }
160        }
161    }
162
163    protected void addValidatorsForProperties( Document parent,
164                                               Path parentPath,
165                                               Problems problems,
166                                               CompositeValidator validators ) {
167        Document properties = parent.getDocument("properties");
168        Set<String> propertiesWithSchemas = new HashSet<>();
169        if (properties != null && properties.size() != 0) {
170            for (Field field : properties.fields()) {
171                String name = field.getName();
172                Object value = field.getValue();
173                Path path = Paths.path(parentPath, "properties", name);
174                if (!(value instanceof Document)) {
175                    problems.recordError(path, "Expected a nested object");
176                }
177                Document propertySchema = (Document)value;
178                Validator propertyValidator = create(propertySchema, path);
179                if (propertyValidator != null) {
180                    validators.add(new PropertyValidator(name, propertyValidator));
181                }
182                propertiesWithSchemas.add(name);
183            }
184        }
185
186        // Check the additional properties ...
187        boolean additionalPropertiesAllowed = parent.getBoolean("additionalProperties", true);
188        if (!additionalPropertiesAllowed) {
189            validators.add(new NoOtherAllowedPropertiesValidator(propertiesWithSchemas));
190        } else {
191            Document additionalSchema = parent.getDocument("additionalProperties");
192            if (additionalSchema != null) {
193                Path path = parentPath.with("additionalProperties");
194                Validator additionalValidator = create(additionalSchema, path);
195                if (additionalValidator != null) {
196                    validators.add(new AllowedPropertiesValidator(propertiesWithSchemas, additionalValidator));
197                }
198            }
199            // Otherwise, additional properties are allowed so we need to do nothing
200        }
201
202    }
203
204    protected void addValidatorsForPatternProperties( Document parent,
205                                                      Path parentPath,
206                                                      Problems problems,
207                                                      CompositeValidator validators ) {
208        Document properties = parent.getDocument("patternProperties");
209        if (properties != null && properties.size() != 0) {
210            for (Field field : properties.fields()) {
211                String name = field.getName();
212                Object value = field.getValue();
213                Path path = Paths.path(parentPath, "patternProperties", name);
214                if (!(value instanceof Document)) {
215                    problems.recordError(path, "Expected a nested object");
216                }
217                Document propertySchema = (Document)value;
218                try {
219                    Pattern namePattern = Pattern.compile(name);
220                    Validator propertyValidator = create(propertySchema, path);
221                    if (propertyValidator != null) {
222                        validators.add(new PatternPropertyValidator(namePattern, propertyValidator));
223                    }
224                } catch (PatternSyntaxException e) {
225                    problems.recordError(path, "Expected the field name to be a regular expression");
226                }
227            }
228        }
229    }
230
231    protected void addValidatorsForItems( Document parent,
232                                          Path parentPath,
233                                          Problems problems,
234                                          CompositeValidator validators ) {
235        Object items = parent.get("items");
236        if (Null.matches(items)) return;
237
238        Path path = parentPath.with("items");
239        String requiredName = parentPath.getLast();
240        if (requiredName == null) return;
241
242        // Either a schema or an array of schemas ...
243        if (items instanceof Document) {
244            Document schema = (Document)items;
245            Validator validator = create(schema, path);
246            if (validator != null) {
247                validators.add(new AllItemsMatchValidator(requiredName, validator));
248            }
249        } else if (items instanceof List<?>) {
250            // This is called "tuple typing" in the spec, and can also have 'additionalItems' ...
251            List<?> array = (List<?>)items;
252            List<Validator> itemValidators = new ArrayList<>(array.size());
253            for (Object item : array) {
254                if (item instanceof Document) {
255                    Validator validator = create((Document)item, path);
256                    if (validator != null) {
257                        itemValidators.add(validator);
258                    }
259                }
260            }
261            // Check the additional items ...
262            boolean additionalItemsAllowed = parent.getBoolean("additionalItems", true);
263            Validator additionalItemsValidator = null;
264            if (!additionalItemsAllowed) {
265                additionalItemsValidator = new NotValidValidator();
266            } else {
267                // additional items are allowed, but check whether there is a schema for the additional items ...
268                Document additionalItems = parent.getDocument("additionalItems");
269                if (additionalItems != null) {
270                    Path additionalItemsPath = parentPath.with("additionalItems");
271                    additionalItemsValidator = create(additionalItems, additionalItemsPath);
272                }
273            }
274
275            if (!itemValidators.isEmpty()) {
276                validators.add(new EachItemMatchesValidator(requiredName, itemValidators, additionalItemsValidator,
277                                                            additionalItemsAllowed));
278            }
279        }
280    }
281
282    protected void addValidatorsForRequired( Document parent,
283                                             Path parentPath,
284                                             Problems problems,
285                                             CompositeValidator validators ) {
286        Boolean required = parent.getBoolean("required", Boolean.FALSE);
287        if (required.booleanValue()) {
288            String requiredName = parentPath.getLast();
289            if (requiredName != null) {
290                validators.add(new RequiredValidator(requiredName));
291            }
292        }
293    }
294
295    protected void addValidatorsForMinimum( Document parent,
296                                            Path parentPath,
297                                            Problems problems,
298                                            CompositeValidator validators ) {
299        Number minimum = parent.getNumber("minimum");
300        if (minimum != null) {
301            String requiredName = parentPath.getLast();
302            if (requiredName != null) {
303                if (parent.getBoolean("exclusiveMinimum", Boolean.FALSE)) {
304                    validators.add(new ExclusiveMinimumValidator(requiredName, minimum));
305                } else {
306                    validators.add(new MinimumValidator(requiredName, minimum));
307                }
308            }
309        }
310    }
311
312    protected void addValidatorsForMaximum( Document parent,
313                                            Path parentPath,
314                                            Problems problems,
315                                            CompositeValidator validators ) {
316        Double maximum = parent.getDouble("maximum");
317        if (maximum != null) {
318            String requiredName = parentPath.getLast();
319            if (requiredName != null) {
320                if (parent.getBoolean("exclusiveMinimum", Boolean.FALSE)) {
321                    validators.add(new ExclusiveMaximumValidator(requiredName, maximum));
322                } else {
323                    validators.add(new MaximumValidator(requiredName, maximum));
324                }
325            }
326        }
327    }
328
329    protected void addValidatorsForMinimumItems( Document parent,
330                                                 Path parentPath,
331                                                 Problems problems,
332                                                 CompositeValidator validators ) {
333        int minimum = parent.getInteger("minItems", 0);
334        if (minimum > 0) {
335            String requiredName = parentPath.getLast();
336            if (requiredName != null) {
337                validators.add(new MinimumItemsValidator(requiredName, minimum));
338            }
339        }
340    }
341
342    protected void addValidatorsForMaximumItems( Document parent,
343                                                 Path parentPath,
344                                                 Problems problems,
345                                                 CompositeValidator validators ) {
346        int maximum = parent.getInteger("maxItems", 0);
347        if (maximum > 0) {
348            String requiredName = parentPath.getLast();
349            if (requiredName != null) {
350                validators.add(new MaximumItemsValidator(requiredName, maximum));
351            }
352        }
353    }
354
355    protected void addValidatorsForUniqueItems( Document parent,
356                                                Path parentPath,
357                                                Problems problems,
358                                                CompositeValidator validators ) {
359        if (parent.getBoolean("uniqueItems", false)) {
360            String requiredName = parentPath.getLast();
361            if (requiredName != null) {
362                validators.add(new UniqueItemsValidator(requiredName));
363            }
364        }
365    }
366
367    protected void addValidatorsForPattern( Document parent,
368                                            Path parentPath,
369                                            Problems problems,
370                                            CompositeValidator validators ) {
371        String regex = parent.getString("pattern");
372        if (regex != null) {
373            String requiredName = parentPath.getLast();
374            if (requiredName != null) {
375                try {
376                    Pattern pattern = Pattern.compile(regex);
377                    validators.add(new PatternValidator(requiredName, pattern));
378                } catch (PatternSyntaxException e) {
379                    problems.recordError(parentPath.with("pattern"),
380                                         "The supplied value '" + regex
381                                         + "' is expected to be a valid regular expression, but there was an error at position "
382                                         + e.getIndex() + ": " + e.getDescription());
383                }
384            }
385        }
386    }
387
388    protected void addValidatorsForMinimumLength( Document parent,
389                                                  Path parentPath,
390                                                  Problems problems,
391                                                  CompositeValidator validators ) {
392        int minimumLength = parent.getInteger("minimumLength", 0);
393        if (minimumLength > 0) {
394            String requiredName = parentPath.getLast();
395            if (requiredName != null) {
396                validators.add(new MinimumLengthValidator(requiredName, minimumLength));
397            }
398        }
399    }
400
401    protected void addValidatorsForMaximumLength( Document parent,
402                                                  Path parentPath,
403                                                  Problems problems,
404                                                  CompositeValidator validators ) {
405        int maximumLength = parent.getInteger("maximumLength", 0);
406        if (maximumLength > 0) {
407            String requiredName = parentPath.getLast();
408            if (requiredName != null) {
409                validators.add(new MaximumLengthValidator(requiredName, maximumLength));
410            }
411        }
412    }
413
414    protected void addValidatorsForEnum( Document parent,
415                                         Path parentPath,
416                                         Problems problems,
417                                         CompositeValidator validators ) {
418        List<?> enumValues = parent.getArray("enum");
419        if (enumValues != null && !enumValues.isEmpty()) {
420            String requiredName = parentPath.getLast();
421            if (requiredName != null) {
422                validators.add(new EnumValidator(requiredName, enumValues));
423            }
424        }
425    }
426
427    protected void addValidatorsForDivisibleBy( Document parent,
428                                                Path parentPath,
429                                                Problems problems,
430                                                CompositeValidator validators ) {
431        Number denominator = parent.getNumber("divisibleBy", 1);
432        if (denominator != null) {
433            int denominatorIntValue = denominator.intValue();
434            if (denominatorIntValue != 0 && denominatorIntValue != 1) {
435                String requiredName = parentPath.getLast();
436                if (requiredName != null) {
437                    validators.add(new DivisibleByValidator(requiredName, denominator.intValue()));
438                }
439            }
440        }
441    }
442
443    protected void addValidatorsForDisallowedTypes( Document parent,
444                                                    Path parentPath,
445                                                    Problems problems,
446                                                    CompositeValidator validators ) {
447        Object disallowed = parent.get("disallowed");
448        if (Null.matches(disallowed)) return;
449        String requiredName = parentPath.getLast();
450        if (requiredName != null) {
451            EnumSet<Type> disallowedTypes = Type.typesWithNames(disallowed);
452            validators.add(new DisallowedTypesValidator(requiredName, disallowedTypes));
453        }
454    }
455
456    protected class TypeValidator implements Validator {
457        private static final long serialVersionUID = 1L;
458        private final Type type;
459
460        public TypeValidator( Type type ) {
461            this.type = type;
462            assert this.type != null;
463        }
464
465        @Override
466        public void validate( Object fieldValue,
467                              String fieldName,
468                              Document document,
469                              Path pathToDocument,
470                              Problems problems,
471                              SchemaDocumentResolver resolver ) {
472            if (fieldValue == null) {
473                if (fieldName != null) {
474                    fieldValue = document.get(fieldName);
475                } else {
476                    // We're supposed to check the whole document is the correct type ...
477                    fieldValue = document;
478                }
479            }
480            if (fieldValue != null) {
481                Type actual = Type.typeFor(fieldValue);
482                if (!type.isEquivalent(actual)) {
483                    // See if the value is convertable ...
484                    Object converted = type.convertValueFrom(fieldValue, actual);
485                    Path pathToField = fieldName != null ? pathToDocument.with(fieldName) : pathToDocument;
486                    String reason = "Field value for '" + pathToField + "' expected to be of type " + type + " but was of type "
487                                    + actual;
488                    if (converted != null) {
489                        // We could convert the value, so record this as a special error ...
490                        problems.recordTypeMismatch(pathToField, reason, actual, fieldValue, type, converted);
491                    } else {
492                        problems.recordError(pathToField, reason);
493                    }
494                } else {
495                    problems.recordSuccess();
496                }
497            }
498        }
499
500        @Override
501        public String toString() {
502            return "Type is '" + type + "'";
503        }
504    }
505
506    protected static interface ValidatorCollection extends Iterable<Validator> {
507    }
508
509    protected class UnionValidator implements Validator, ValidatorCollection {
510        private static final long serialVersionUID = 1L;
511        private final List<Validator> validators;
512
513        public UnionValidator( List<Validator> validators ) {
514            this.validators = validators;
515            assert this.validators != null && !this.validators.isEmpty();
516        }
517
518        @Override
519        public void validate( Object fieldValue,
520                              String fieldName,
521                              Document document,
522                              Path pathToDocument,
523                              Problems problems,
524                              SchemaDocumentResolver resolver ) {
525            // Try each validator with a new problems; the first one to return without any problems passes ...
526            ValidationResult problemsForMostSuccesses = null;
527            int mostSuccesses = -1;
528            for (Validator validator : validators) {
529                ValidationResult newProblems = new ValidationResult();
530                validator.validate(fieldValue, fieldName, document, pathToDocument, newProblems, resolver);
531                if (!newProblems.hasErrors()) {
532                    problems.recordSuccess();
533                    return;
534                }
535                if (newProblems.successCount() > mostSuccesses) {
536                    mostSuccesses = newProblems.successCount();
537                    problemsForMostSuccesses = newProblems;
538                }
539            }
540            // All unioned types had problems, but record the problems with the one that had the most successful validations ...
541            if (problemsForMostSuccesses != null) problemsForMostSuccesses.recordIn(problems);
542        }
543
544        @Override
545        public Iterator<Validator> iterator() {
546            return validators.iterator();
547        }
548
549        @Override
550        public String toString() {
551            return "Union of " + validators.size() + " validators";
552        }
553    }
554
555    protected class ResolvingValidator implements Validator {
556        private static final long serialVersionUID = 1L;
557        private final String schemaUri;
558
559        public ResolvingValidator( String schemaUri ) {
560            this.schemaUri = schemaUri;
561        }
562
563        @Override
564        public void validate( Object fieldValue,
565                              String fieldName,
566                              Document document,
567                              Path pathToDocument,
568                              Problems problems,
569                              SchemaDocumentResolver resolver ) {
570            SchemaDocument resolved = resolver.get(schemaUri, problems);
571            if (resolved == null) {
572                problems.recordError(pathToDocument.with(fieldName), "Unable to find referenced schema '" + schemaUri + "'");
573            } else {
574                problems.recordSuccess();
575                resolved.getValidator().validate(fieldValue, fieldName, document, pathToDocument, problems, resolver);
576            }
577        }
578
579        @Override
580        public String toString() {
581            return "Resolves to schema '" + schemaUri + "'";
582        }
583    }
584
585    protected static class RequiredValidator implements Validator {
586        private static final long serialVersionUID = 1L;
587        private final String propertyName;
588
589        public RequiredValidator( String propertyName ) {
590            this.propertyName = propertyName;
591        }
592
593        @Override
594        public void validate( Object fieldValue,
595                              String fieldName,
596                              Document parent,
597                              Path pathToParent,
598                              Problems problems,
599                              SchemaDocumentResolver resolver ) {
600            if (Null.matches(fieldValue) && fieldName != null) {
601                if (pathToParent.size() == 0) {
602                    problems.recordError(pathToParent.with(fieldName), "The top-level '" + fieldName + "' field is required");
603                } else {
604                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
605                                                                       + "' is required");
606                }
607            } else {
608                problems.recordSuccess();
609            }
610        }
611
612        @Override
613        public String toString() {
614            return "required '" + propertyName + "'";
615        }
616    }
617
618    protected static abstract class NumericValidator implements Validator {
619        private static final long serialVersionUID = 1L;
620        private final String propertyName;
621        private final Number number;
622        private final double value;
623
624        protected NumericValidator( String propertyName,
625                                    Number number ) {
626            this.propertyName = propertyName;
627            this.number = number;
628            this.value = number.doubleValue();
629        }
630
631        @Override
632        public void validate( Object fieldValue,
633                              String fieldName,
634                              Document parent,
635                              Path pathToParent,
636                              Problems problems,
637                              SchemaDocumentResolver resolver ) {
638            if (fieldValue instanceof Number) {
639                Number actualNumber = (Number)fieldValue;
640                double actualValue = actualNumber.doubleValue();
641                if (isValid(value, actualValue)) {
642                    problems.recordSuccess();
643                } else {
644                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
645                                                                       + "' is '" + actualNumber + "' but must be "
646                                                                       + ruleDescription() + " '" + number + "'");
647                }
648            }
649            // otherwise the value is not a number and the minimum doesn't apply
650        }
651
652        /**
653         * Evaluate whether the actual value and expected value violate the schema rule.
654         * 
655         * @param expectedValue the expected value
656         * @param actualValue the actual value
657         * @return true if the value is valid, or false if there is an error
658         */
659        protected abstract boolean isValid( double expectedValue,
660                                            double actualValue );
661
662        protected abstract String ruleDescription();
663
664        @Override
665        public String toString() {
666            return "'" + propertyName + "' is '" + ruleDescription() + " '" + number + "'";
667        }
668    }
669
670    protected static class MinimumValidator extends NumericValidator {
671        private static final long serialVersionUID = 1L;
672
673        public MinimumValidator( String propertyName,
674                                 Number minimum ) {
675            super(propertyName, minimum);
676        }
677
678        @Override
679        protected boolean isValid( double minimum,
680                                   double actualValue ) {
681            return actualValue >= minimum;
682        }
683
684        @Override
685        protected String ruleDescription() {
686            return "greater than or equal to";
687        }
688    }
689
690    /**
691     * Validation rule that states fails if the actual value is equal to or less than the minimum value.
692     */
693    protected static class ExclusiveMinimumValidator extends NumericValidator {
694        private static final long serialVersionUID = 1L;
695
696        public ExclusiveMinimumValidator( String propertyName,
697                                          Number minimum ) {
698            super(propertyName, minimum);
699        }
700
701        @Override
702        protected boolean isValid( double minimum,
703                                   double actualValue ) {
704            return actualValue > minimum;
705        }
706
707        @Override
708        protected String ruleDescription() {
709            return "greater than";
710        }
711    }
712
713    protected static class MaximumValidator extends NumericValidator {
714        private static final long serialVersionUID = 1L;
715
716        public MaximumValidator( String propertyName,
717                                 Number maximum ) {
718            super(propertyName, maximum);
719        }
720
721        @Override
722        protected boolean isValid( double maximum,
723                                   double actualValue ) {
724            return actualValue <= maximum;
725        }
726
727        @Override
728        protected String ruleDescription() {
729            return "less than or equal to";
730        }
731    }
732
733    protected static class ExclusiveMaximumValidator extends NumericValidator {
734        private static final long serialVersionUID = 1L;
735
736        public ExclusiveMaximumValidator( String propertyName,
737                                          Number maximum ) {
738            super(propertyName, maximum);
739        }
740
741        @Override
742        protected boolean isValid( double maximum,
743                                   double actualValue ) {
744            return actualValue < maximum;
745        }
746
747        @Override
748        protected String ruleDescription() {
749            return "less than";
750        }
751    }
752
753    protected static class MinimumLengthValidator implements Validator {
754        private static final long serialVersionUID = 1L;
755        private final String propertyName;
756        private final int minimumLength;
757
758        public MinimumLengthValidator( String propertyName,
759                                       int minimumLength ) {
760            this.propertyName = propertyName;
761            this.minimumLength = minimumLength;
762        }
763
764        @Override
765        public void validate( Object fieldValue,
766                              String fieldName,
767                              Document parent,
768                              Path pathToParent,
769                              Problems problems,
770                              SchemaDocumentResolver resolver ) {
771            if (fieldValue instanceof String || fieldValue instanceof Symbol) {
772                String value = fieldValue.toString();
773                if (value.length() < minimumLength) {
774                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
775                                                                       + "' had " + value.length()
776                                                                       + " characters, but was expected to have at least "
777                                                                       + minimumLength);
778                } else {
779                    problems.recordSuccess();
780                }
781            }
782        }
783
784        @Override
785        public String toString() {
786            return "'" + propertyName + "' has a minimum length of " + minimumLength;
787        }
788    }
789
790    protected static class MaximumLengthValidator implements Validator {
791        private static final long serialVersionUID = 1L;
792        private final String propertyName;
793        private final int maximumLength;
794
795        public MaximumLengthValidator( String propertyName,
796                                       int maximumLength ) {
797            this.propertyName = propertyName;
798            this.maximumLength = maximumLength;
799        }
800
801        @Override
802        public void validate( Object fieldValue,
803                              String fieldName,
804                              Document parent,
805                              Path pathToParent,
806                              Problems problems,
807                              SchemaDocumentResolver resolver ) {
808            if (fieldValue instanceof String || fieldValue instanceof Symbol) {
809                String value = fieldValue.toString();
810                if (value.length() > maximumLength) {
811                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
812                                                                       + "' had " + value.length()
813                                                                       + " characters, but was expected to have no more than "
814                                                                       + maximumLength);
815                } else {
816                    problems.recordSuccess();
817                }
818            }
819        }
820
821        @Override
822        public String toString() {
823            return "'" + propertyName + "' has a maximum length of " + maximumLength;
824        }
825    }
826
827    protected static class DivisibleByValidator implements Validator {
828        private static final long serialVersionUID = 1L;
829        private final String propertyName;
830        private final int denominator;
831
832        public DivisibleByValidator( String propertyName,
833                                     int denominator ) {
834            this.propertyName = propertyName;
835            this.denominator = denominator;
836            assert this.denominator != 0;
837        }
838
839        @Override
840        public void validate( Object fieldValue,
841                              String fieldName,
842                              Document parent,
843                              Path pathToParent,
844                              Problems problems,
845                              SchemaDocumentResolver resolver ) {
846            if (Null.matches(fieldValue)) return;
847            if (fieldValue instanceof Integer) {
848                int value = ((Integer)fieldValue).intValue();
849                if (value % denominator != 0) {
850                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
851                                                                       + "' had a value of " + value
852                                                                       + " and was not divisible by " + denominator);
853                } else {
854                    problems.recordSuccess();
855                }
856            } else if (fieldValue instanceof Long) {
857                long value = ((Long)fieldValue).longValue();
858                if (value % denominator != 0L) {
859                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
860                                                                       + "' had a value of " + value
861                                                                       + " and was not divisible by " + denominator);
862                } else {
863                    problems.recordSuccess();
864                }
865            } else if (fieldValue instanceof Short) {
866                int value = ((Short)fieldValue).intValue();
867                if (value % denominator != 0) {
868                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
869                                                                       + "' had a value of " + value
870                                                                       + " and was not divisible by " + denominator);
871                } else {
872                    problems.recordSuccess();
873                }
874            } else if (fieldValue instanceof Float) {
875                float value = ((Float)fieldValue).floatValue();
876                if (value % denominator != 0.0f) {
877                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
878                                                                       + "' had a value of " + value
879                                                                       + " and was not divisible by " + denominator);
880                } else {
881                    problems.recordSuccess();
882                }
883            } else if (fieldValue instanceof Double) {
884                double value = ((Double)fieldValue).floatValue();
885                if (value % denominator != 0.0d) {
886                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
887                                                                       + "' had a value of " + value
888                                                                       + " and was not divisible by " + denominator);
889                } else {
890                    problems.recordSuccess();
891                }
892            }
893        }
894
895        @Override
896        public String toString() {
897            return "'" + propertyName + "' must be divisible by " + denominator;
898        }
899    }
900
901    protected static abstract class ItemCountValidator implements Validator {
902        private static final long serialVersionUID = 1L;
903        private final String propertyName;
904        private final int number;
905
906        protected ItemCountValidator( String propertyName,
907                                      int number ) {
908            this.propertyName = propertyName;
909            this.number = number;
910        }
911
912        @Override
913        public void validate( Object fieldValue,
914                              String fieldName,
915                              Document parent,
916                              Path pathToParent,
917                              Problems problems,
918                              SchemaDocumentResolver resolver ) {
919            if (fieldValue instanceof List) {
920                List<?> array = (List<?>)fieldValue;
921                if (evaluate(number, array.size())) {
922                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
923                                                                       + "' has '" + array.size() + "' values but should have "
924                                                                       + ruleDescription() + " '" + number + "'");
925                } else {
926                    problems.recordSuccess();
927                }
928            }
929            // otherwise the value is not a number and the minimum doesn't apply
930        }
931
932        protected abstract boolean evaluate( double value,
933                                             double actualValue );
934
935        protected abstract String ruleDescription();
936
937        @Override
938        public String toString() {
939            return "'" + propertyName + "' has '" + ruleDescription() + " '" + number + "' items";
940        }
941    }
942
943    protected static class MinimumItemsValidator extends ItemCountValidator {
944        private static final long serialVersionUID = 1L;
945
946        public MinimumItemsValidator( String propertyName,
947                                      int minimum ) {
948            super(propertyName, minimum);
949        }
950
951        @Override
952        protected boolean evaluate( double minimumCount,
953                                    double actualCount ) {
954            return minimumCount < actualCount;
955        }
956
957        @Override
958        protected String ruleDescription() {
959            return "at least";
960        }
961    }
962
963    protected static class MaximumItemsValidator extends ItemCountValidator {
964        private static final long serialVersionUID = 1L;
965
966        public MaximumItemsValidator( String propertyName,
967                                      int maximum ) {
968            super(propertyName, maximum);
969        }
970
971        @Override
972        protected boolean evaluate( double maximumCount,
973                                    double actualCount ) {
974            return maximumCount < actualCount;
975        }
976
977        @Override
978        protected String ruleDescription() {
979            return "no more than";
980        }
981    }
982
983    protected static class UniqueItemsValidator implements Validator {
984        private static final long serialVersionUID = 1L;
985        private final String propertyName;
986
987        public UniqueItemsValidator( String propertyName ) {
988            this.propertyName = propertyName;
989        }
990
991        @Override
992        public void validate( Object fieldValue,
993                              String fieldName,
994                              Document parent,
995                              Path pathToParent,
996                              Problems problems,
997                              SchemaDocumentResolver resolver ) {
998            // This only applies if the value is a JSON array ...
999            if (fieldValue instanceof List) {
1000                List<?> array = (List<?>)fieldValue;
1001                Set<?> uniqueValues = new HashSet<>(array);
1002                int numDups = array.size() - uniqueValues.size();
1003                if (numDups != 0) {
1004                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
1005                                                                       + "' must contain unique values, but contains " + numDups
1006                                                                       + " duplicate values");
1007                } else {
1008                    problems.recordSuccess();
1009                }
1010            }
1011        }
1012
1013        @Override
1014        public String toString() {
1015            return "'" + propertyName + "' contains unique items";
1016        }
1017    }
1018
1019    protected static class PatternValidator implements Validator {
1020        private static final long serialVersionUID = 1L;
1021        private final String propertyName;
1022        private final Pattern pattern;
1023
1024        public PatternValidator( String propertyName,
1025                                 Pattern pattern ) {
1026            this.propertyName = propertyName;
1027            this.pattern = pattern;
1028        }
1029
1030        @Override
1031        public void validate( Object fieldValue,
1032                              String fieldName,
1033                              Document parent,
1034                              Path pathToParent,
1035                              Problems problems,
1036                              SchemaDocumentResolver resolver ) {
1037            if (fieldValue instanceof String || fieldValue instanceof Symbol) {
1038                String value = fieldValue.toString();
1039                Matcher matcher = pattern.matcher(value);
1040                if (!matcher.matches()) {
1041                    problems.recordError(pathToParent.with(fieldName),
1042                                         "The '" + fieldName + "' field on '" + pathToParent
1043                                         + "' failed match the pattern specified by '" + pattern.pattern() + "'");
1044                } else {
1045                    problems.recordSuccess();
1046                }
1047            }
1048        }
1049
1050        @Override
1051        public String toString() {
1052            return "'" + propertyName + "' matches pattern '" + pattern.pattern() + "'";
1053        }
1054    }
1055
1056    protected static class EnumValidator implements Validator {
1057        private static final long serialVersionUID = 1L;
1058        private final String propertyName;
1059        private final Set<String> values;
1060
1061        public EnumValidator( String propertyName,
1062                              Collection<?> values ) {
1063            this.propertyName = propertyName;
1064            this.values = values.stream().map(object -> object.toString().toLowerCase()).collect(Collectors.toSet());
1065        }
1066
1067        @Override
1068        public void validate( Object fieldValue,
1069                              String fieldName,
1070                              Document parent,
1071                              Path pathToParent,
1072                              Problems problems,
1073                              SchemaDocumentResolver resolver ) {
1074            if (!propertyName.equals(fieldName)) return;
1075            // This only applies if the value is a JSON array ...
1076            if (fieldValue instanceof List) {
1077                for (Object value : (List<?>)fieldValue) {
1078                    if (values.contains(value.toString().toLowerCase())) {
1079                        problems.recordSuccess();
1080                    } else {
1081                        problems.recordError(pathToParent.with(fieldName),
1082                                             "The '" + fieldName + "' field on '" + pathToParent + "' contains a value '" + value
1083                                             + "' in the array that is not part of the enumeration: " + values);
1084                    }
1085                }
1086            } else if (fieldValue != null) {
1087                if (values.contains(fieldValue.toString().toLowerCase())) {
1088                    problems.recordSuccess();
1089                } else {
1090                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
1091                                                                       + "' has a value of '" + fieldValue
1092                                                                       + "' that is not part of the enumeration: " + values);
1093                }
1094            }
1095        }
1096
1097        @Override
1098        public String toString() {
1099            return "'" + propertyName + "' contains values from enumeration: " + values;
1100        }
1101    }
1102
1103    protected static class DisallowedTypesValidator implements Validator {
1104        private static final long serialVersionUID = 1L;
1105        private final String propertyName;
1106        private final EnumSet<Type> disallowedTypes;
1107
1108        public DisallowedTypesValidator( String propertyName,
1109                                         EnumSet<Type> disallowedTypes ) {
1110            this.propertyName = propertyName;
1111            this.disallowedTypes = disallowedTypes;
1112        }
1113
1114        @Override
1115        public void validate( Object fieldValue,
1116                              String fieldName,
1117                              Document parent,
1118                              Path pathToParent,
1119                              Problems problems,
1120                              SchemaDocumentResolver resolver ) {
1121            Type type = Type.typeFor(fieldValue);
1122            if (type != Type.NULL) {
1123                if (disallowedTypes.contains(type)) {
1124                    problems.recordError(pathToParent.with(fieldName), "The '" + fieldName + "' field on '" + pathToParent
1125                                                                       + "' contains a value '" + fieldValue + "' whose type '"
1126                                                                       + type + "' is disallowed.");
1127                } else {
1128                    problems.recordSuccess();
1129                }
1130            }
1131        }
1132
1133        @Override
1134        public String toString() {
1135            return "'" + propertyName + "' may not have values with the types " + disallowedTypes;
1136        }
1137    }
1138
1139    /**
1140     * The {@link Validator} for item values that should all match a single schema.
1141     * 
1142     * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc.
1143     * @since 5.1
1144     */
1145    protected static class AllItemsMatchValidator implements Validator {
1146        private static final long serialVersionUID = 1L;
1147        private final String propertyName;
1148        private final Validator itemValidator;
1149        private final SingleProblem itemProblems = new SingleProblem();
1150
1151        public AllItemsMatchValidator( String propertyName,
1152                                       Validator itemValidator ) {
1153            this.propertyName = propertyName;
1154            this.itemValidator = itemValidator;
1155        }
1156
1157        @Override
1158        public void validate( Object fieldValue,
1159                              String fieldName,
1160                              Document parent,
1161                              Path pathToParent,
1162                              Problems problems,
1163                              SchemaDocumentResolver resolver ) {
1164            if (fieldValue instanceof List) {
1165                // Each item in the list must match the itemValidator or additionalItemsValidator ...
1166                List<?> items = (List<?>)fieldValue;
1167                Path path = pathToParent.with(fieldName);
1168                int i = 1;
1169                boolean success = true;
1170                for (Object item : items) {
1171                    itemProblems.clear();
1172                    itemValidator.validate(item, fieldName, parent, pathToParent, itemProblems, resolver);
1173                    if (itemProblems.hasProblem()) {
1174                        problems.recordError(path, "The '" + fieldName + "' field on '" + pathToParent
1175                                                   + "' is an array, but the " + i + th(i)
1176                                                   + " item does not satisfy the schema for the " + i + th(i) + " item");
1177                        success = false;
1178                    }
1179                    ++i;
1180                }
1181                if (success) problems.recordSuccess();
1182            } else if (parent instanceof List) {
1183                // we are dealing with an optional array of items
1184                List<?> items = (List<?>)parent;
1185                int i = 1;
1186                boolean success = true;
1187                for (Object item : items) {
1188                    itemProblems.clear();
1189                    if (item instanceof Document) {
1190                        itemValidator.validate(null, null, (Document)item, pathToParent, itemProblems, resolver);
1191                        if (itemProblems.hasProblem()) {
1192                            success = false;
1193                        }
1194                    } else {
1195                        fieldName = item.toString();
1196                        Path path = pathToParent.with(fieldName);
1197                        itemValidator.validate(item, fieldName, parent, pathToParent, itemProblems, resolver);
1198                        if (itemProblems.hasProblem()) {
1199                            problems.recordError(path, "The '" + fieldName + "' field on '" + pathToParent
1200                                                       + "' is an array, but the " + i + th(i)
1201                                                       + " item does not satisfy the schema for the " + i + th(i) + " item");
1202                            success = false;
1203                        }
1204                    }
1205                    ++i;
1206                }
1207                if (success) problems.recordSuccess();
1208            }
1209
1210        }
1211
1212        @Override
1213        public String toString() {
1214            return "'" + propertyName + "' may be an array with items matching the schema: " + itemValidator;
1215        }
1216    }
1217
1218    /**
1219     * The {@link Validator} for "tuple typing", when item values should each match a corresponding schema or, if applicable, an
1220     * additional items schema.
1221     * 
1222     * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc.
1223     * @since 5.1
1224     */
1225    protected static class EachItemMatchesValidator implements Validator {
1226        private static final long serialVersionUID = 1L;
1227        private final String propertyName;
1228        private final List<Validator> itemValidators;
1229        private final Validator additionalItemsValidator;
1230        private final SingleProblem itemProblems = new SingleProblem();
1231        private final boolean additionalItemsAllowed;
1232
1233        public EachItemMatchesValidator( String propertyName,
1234                                         List<Validator> itemValidators,
1235                                         Validator additionalItemsValidator,
1236                                         boolean additionalItemsAllowed ) {
1237            this.propertyName = propertyName;
1238            this.itemValidators = itemValidators;
1239            this.additionalItemsValidator = additionalItemsValidator;
1240            this.additionalItemsAllowed = additionalItemsAllowed;
1241        }
1242
1243        @Override
1244        public void validate( Object fieldValue,
1245                              String fieldName,
1246                              Document parent,
1247                              Path pathToParent,
1248                              Problems problems,
1249                              SchemaDocumentResolver resolver ) {
1250            if (fieldValue instanceof List) {
1251                // Each item in the list must match the itemValidator or additionalItemsValidator ...
1252                List<?> items = (List<?>)fieldValue;
1253                Path path = pathToParent.with(fieldName);
1254                int i = 0;
1255                Iterator<?> itemIterator = items.iterator();
1256                Iterator<Validator> itemValidatorIterator = itemValidators.iterator();
1257                boolean success = true;
1258                while (itemIterator.hasNext() && itemValidatorIterator.hasNext()) {
1259                    ++i;
1260                    Object item = itemIterator.next();
1261                    Validator itemValidator = itemValidatorIterator.next();
1262                    itemValidator.validate(item, fieldName, parent, pathToParent, itemProblems, resolver);
1263                }
1264                if (additionalItemsAllowed && additionalItemsValidator != null) {
1265                    while (itemIterator.hasNext()) {
1266                        ++i;
1267                        Object item = itemIterator.next();
1268                        itemProblems.clear();
1269                        additionalItemsValidator.validate(item, fieldName, parent, pathToParent, itemProblems, resolver);
1270                        if (itemProblems.hasProblem()) {
1271                            problems.recordError(path,
1272                                                 "The '"
1273                                                 + fieldName
1274                                                 + "' field on '"
1275                                                 + pathToParent
1276                                                 + "' is an array, but the "
1277                                                 + i
1278                                                 + th(i)
1279                                                 + " item does have a corresponding schema and does not satisfy the additional items schema)");
1280                            success = false;
1281                        }
1282                    }
1283                } else if (!additionalItemsAllowed) {
1284                    while (itemIterator.hasNext()) {
1285                        ++i;
1286                        problems.recordError(path,
1287                                             "The '" + fieldName + "' field on '" + pathToParent + "' is an array, but the " + i
1288                                             + th(i)
1289                                             + " item does have a corresponding schema (and no additional items were specified)");
1290                        success = false;
1291                    }
1292                }
1293                if (success) problems.recordSuccess();
1294            }
1295        }
1296
1297        @Override
1298        public String toString() {
1299            return "'" + propertyName + "' may be an array with items matching the schemas: " + itemValidators
1300                   + (additionalItemsValidator == null ? "" : " or the additional items schema " + additionalItemsValidator);
1301        }
1302    }
1303
1304    protected static class NotValidValidator implements Validator {
1305        private static final long serialVersionUID = 1L;
1306
1307        public NotValidValidator() {
1308        }
1309
1310        @Override
1311        public void validate( Object fieldValue,
1312                              String fieldName,
1313                              Document parent,
1314                              Path pathToParent,
1315                              Problems problems,
1316                              SchemaDocumentResolver resolver ) {
1317            problems.recordError(pathToParent, "");
1318        }
1319
1320        @Override
1321        public String toString() {
1322            return "not valid";
1323        }
1324    }
1325
1326    protected static String th( int i ) {
1327        switch (i) {
1328            case 1:
1329                return "st";
1330            case 2:
1331                return "nd";
1332            case 3:
1333                return "rd";
1334        }
1335        return "th";
1336    }
1337
1338    protected static RequiredValidator getRequiredValidator( Validator validator ) {
1339        if (validator instanceof RequiredValidator) return (RequiredValidator)validator;
1340        if (validator instanceof ValidatorCollection) {
1341            for (Validator val : ((ValidatorCollection)validator)) {
1342                if (val instanceof RequiredValidator) return (RequiredValidator)val;
1343            }
1344        }
1345        return null;
1346    }
1347
1348    protected static class PropertyValidator implements Validator {
1349        private static final long serialVersionUID = 1L;
1350        private final String propertyName;
1351        private final Validator validator;
1352        private final RequiredValidator required;
1353
1354        public PropertyValidator( String propertyName,
1355                                  Validator validator ) {
1356            this.propertyName = propertyName;
1357            this.validator = validator;
1358            this.required = getRequiredValidator(validator);
1359        }
1360
1361        @Override
1362        public void validate( Object fieldValue,
1363                              String fieldName,
1364                              Document parent,
1365                              Path pathToParent,
1366                              Problems problems,
1367                              SchemaDocumentResolver resolver ) {
1368            if (fieldName == null) {
1369                fieldName = propertyName;
1370            }
1371            if (fieldValue == null) {
1372                fieldValue = parent.get(propertyName);
1373            }
1374            if (fieldValue == null) {
1375                if (required != null) {
1376                    // The field is required ...
1377                    required.validate(fieldValue, fieldName, parent, pathToParent, problems, resolver);
1378                }
1379                return;
1380            }
1381            if (fieldValue instanceof Document) {
1382                validator.validate(null, null, (Document)fieldValue, pathToParent.with(fieldName), problems, resolver);
1383            } else {
1384                validator.validate(fieldValue, fieldName, parent, pathToParent, problems, resolver);
1385            }
1386        }
1387
1388        @Override
1389        public String toString() {
1390            return "property '" + propertyName + "': " + validator.toString();
1391        }
1392    }
1393
1394    protected static class PatternPropertyValidator implements Validator {
1395        private static final long serialVersionUID = 1L;
1396        private final Pattern propertyNamePattern;
1397        private final Validator validator;
1398
1399        public PatternPropertyValidator( Pattern propertyNamePattern,
1400                                         Validator validator ) {
1401            this.propertyNamePattern = propertyNamePattern;
1402            this.validator = validator;
1403        }
1404
1405        @Override
1406        public void validate( Object fieldValue,
1407                              String fieldName,
1408                              Document parent,
1409                              Path pathToParent,
1410                              Problems problems,
1411                              SchemaDocumentResolver resolver ) {
1412            if (fieldValue == null) return;
1413            Matcher matcher = propertyNamePattern.matcher(fieldName);
1414            if (matcher.matches()) {
1415                // Apply the validator to the field value ...
1416                validator.validate(fieldValue, fieldName, parent, pathToParent, problems, resolver);
1417            }
1418        }
1419
1420        @Override
1421        public String toString() {
1422            return "pattern property '" + propertyNamePattern.pattern() + "': " + validator.toString();
1423        }
1424    }
1425
1426    protected static class AllowedPropertiesValidator implements Validator {
1427        private static final long serialVersionUID = 1L;
1428        private final Set<String> allowedPropertyNames;
1429        private final Validator validator;
1430
1431        public AllowedPropertiesValidator( Set<String> allowedPropertyNames,
1432                                           Validator validator ) {
1433            this.allowedPropertyNames = allowedPropertyNames;
1434            this.validator = validator;
1435        }
1436
1437        @Override
1438        public void validate( Object fieldValue,
1439                              String fieldName,
1440                              Document parent,
1441                              Path pathToParent,
1442                              Problems problems,
1443                              SchemaDocumentResolver resolver ) {
1444            if (fieldName != null && !allowedPropertyNames.contains(fieldName)) {
1445                // Then the field is not handled by an explicit schema, so we need to check it here
1446                validator.validate(fieldValue, fieldName, parent, pathToParent, problems, resolver);
1447            } else if (fieldName == null) {
1448                // we need to validate each defined additional property which has a schema
1449                for (Field field : parent.fields()) {
1450                    if (field.getValue() instanceof Document) {
1451                        validator.validate(null,
1452                                           null,
1453                                           (Document)field.getValue(),
1454                                           pathToParent.with(field.getName()),
1455                                           problems,
1456                                           resolver);
1457                    } else {
1458                        validator.validate(field.getValue(), field.getName(), parent, pathToParent, problems, resolver);
1459                    }
1460                }
1461            }
1462        }
1463
1464        @Override
1465        public String toString() {
1466            return "additional properties allowed: " + validator.toString();
1467        }
1468    }
1469
1470    protected static class NoOtherAllowedPropertiesValidator implements Validator {
1471        private static final long serialVersionUID = 1L;
1472        private final Set<String> allowedPropertyNames;
1473
1474        public NoOtherAllowedPropertiesValidator( Set<String> allowedPropertyNames ) {
1475            this.allowedPropertyNames = allowedPropertyNames;
1476        }
1477
1478        @Override
1479        public void validate( Object fieldValue,
1480                              String fieldName,
1481                              Document parent,
1482                              Path pathToParent,
1483                              Problems problems,
1484                              SchemaDocumentResolver resolver ) {
1485            if (fieldValue == null) {
1486                if (fieldName == null) {
1487                    // Go through all of the fields in the document ...
1488                    for (Field field : parent.fields()) {
1489                        validate(field.getValue(), field.getName(), parent, pathToParent, problems, resolver);
1490                    }
1491                }
1492            } else {
1493                if (!allowedPropertyNames.contains(fieldName)) {
1494                    // Then the field is not handled by an explicit schema, so it's not allowed ...
1495                    problems.recordError(pathToParent.with(fieldName),
1496                                         "The '" + fieldName + "' field on '" + pathToParent
1497                                         + "' is not defined in the schema and the schema does not allow additional properties.");
1498                } else {
1499                    problems.recordSuccess();
1500                }
1501            }
1502        }
1503
1504        @Override
1505        public String toString() {
1506            return "additional properties not allowed";
1507        }
1508    }
1509
1510    protected static class CompositeValidator implements Validator, ValidatorCollection {
1511        private static final long serialVersionUID = 1L;
1512
1513        private final List<Validator> validators = new ArrayList<>();
1514
1515        public CompositeValidator() {
1516        }
1517
1518        protected void add( Validator validator ) {
1519            this.validators.add(validator);
1520        }
1521
1522        protected int size() {
1523            return this.validators.size();
1524        }
1525
1526        protected Validator getFirst() {
1527            return this.validators.get(0);
1528        }
1529
1530        @Override
1531        public void validate( Object fieldValue,
1532                              String fieldName,
1533                              Document parent,
1534                              Path pathToParent,
1535                              Problems problems,
1536                              SchemaDocumentResolver resolver ) {
1537            for (Validator validator : validators) {
1538                try {
1539                    validator.validate(fieldValue, fieldName, parent, pathToParent, problems, resolver);
1540                } catch (Throwable t) {
1541                    problems.recordError(pathToParent, t.getMessage(), t);
1542                }
1543            }
1544        }
1545
1546        @Override
1547        public Iterator<Validator> iterator() {
1548            return validators.iterator();
1549        }
1550
1551        @Override
1552        public String toString() {
1553            StringBuilder sb = new StringBuilder();
1554            for (Validator validator : validators) {
1555                sb.append(validator.toString());
1556                sb.append("\n");
1557            }
1558            return sb.toString();
1559        }
1560    }
1561
1562    protected static class SingleProblem implements Problems {
1563        private SchemaLibrary.ProblemType type;
1564        private Path path;
1565        private String message;
1566        private Throwable exception;
1567        private Object actualValue;
1568        private Object convertedValue;
1569        private Type actualType;
1570        private Type requiredType;
1571        private boolean mismatch = false;
1572        private boolean success = false;
1573
1574        @Override
1575        public void recordSuccess() {
1576            success = true;
1577        }
1578
1579        @Override
1580        public void recordError( Path path,
1581                                 String message ) {
1582            this.type = SchemaLibrary.ProblemType.ERROR;
1583            this.path = path;
1584            this.message = message;
1585            this.exception = null;
1586            this.actualValue = null;
1587            this.convertedValue = null;
1588            this.actualType = null;
1589            this.requiredType = null;
1590            this.mismatch = false;
1591            this.success = false;
1592        }
1593
1594        @Override
1595        public void recordError( Path path,
1596                                 String message,
1597                                 Throwable exception ) {
1598            this.type = SchemaLibrary.ProblemType.ERROR;
1599            this.path = path;
1600            this.message = message;
1601            this.exception = exception;
1602            this.actualValue = null;
1603            this.convertedValue = null;
1604            this.actualType = null;
1605            this.requiredType = null;
1606            this.mismatch = false;
1607            this.success = false;
1608        }
1609
1610        @Override
1611        public void recordWarning( Path path,
1612                                   String message ) {
1613            this.type = SchemaLibrary.ProblemType.WARNING;
1614            this.path = path;
1615            this.message = message;
1616            this.exception = null;
1617            this.actualValue = null;
1618            this.convertedValue = null;
1619            this.actualType = null;
1620            this.requiredType = null;
1621            this.mismatch = false;
1622            this.success = false;
1623        }
1624
1625        @Override
1626        public void recordTypeMismatch( Path path,
1627                                        String message,
1628                                        Type actualType,
1629                                        Object actualValue,
1630                                        Type requiredType,
1631                                        Object convertedValue ) {
1632            this.type = SchemaLibrary.ProblemType.ERROR;
1633            this.path = path;
1634            this.message = message;
1635            this.exception = null;
1636            this.actualValue = actualValue;
1637            this.convertedValue = convertedValue;
1638            this.actualType = actualType;
1639            this.requiredType = requiredType;
1640            this.mismatch = true;
1641            this.success = false;
1642        }
1643
1644        public boolean hasProblem() {
1645            return this.type != null;
1646        }
1647
1648        public void clear() {
1649            this.type = null;
1650            this.success = false;
1651        }
1652    }
1653}