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}