From 79acd4d00fc917f58de15cce62ce872a8f56d9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klas=20Kala=C3=9F?= Date: Fri, 17 Nov 2017 18:13:42 +0100 Subject: [PATCH 1/2] Support custom meta schemas with custom keywords and formats Refactored so that the library supports multiple meta schemas and users of the library can add custom meta schemas as well as controlling aspects like e.g. fetching via a URL. --- pom.xml | 30 +-- .../com/networknt/schema/AbstractFormat.java | 25 ++ .../schema/AbstractJsonValidator.java | 36 +++ .../com/networknt/schema/AbstractKeyword.java | 15 ++ .../schema/AdditionalPropertiesValidator.java | 21 +- .../com/networknt/schema/AllOfValidator.java | 7 +- .../com/networknt/schema/AnyOfValidator.java | 7 +- .../networknt/schema/BaseJsonValidator.java | 37 +-- .../schema/CustomErrorMessageType.java | 31 +++ .../schema/DependenciesValidator.java | 7 +- .../com/networknt/schema/EnumValidator.java | 5 +- .../networknt/schema/ErrorMessageType.java | 20 ++ .../java/com/networknt/schema/Format.java | 12 + .../com/networknt/schema/FormatKeyword.java | 40 +++ .../com/networknt/schema/FormatValidator.java | 53 +--- .../com/networknt/schema/ItemsValidator.java | 11 +- .../com/networknt/schema/JsonMetaSchema.java | 244 ++++++++++++++++++ .../java/com/networknt/schema/JsonSchema.java | 88 +++---- .../networknt/schema/JsonSchemaException.java | 4 + .../networknt/schema/JsonSchemaFactory.java | 186 ++++++++++--- .../com/networknt/schema/JsonValidator.java | 1 + .../java/com/networknt/schema/Keyword.java | 10 + .../networknt/schema/MaxItemsValidator.java | 5 +- .../networknt/schema/MaxLengthValidator.java | 5 +- .../schema/MaxPropertiesValidator.java | 5 +- .../networknt/schema/MaximumValidator.java | 5 +- .../networknt/schema/MinItemsValidator.java | 5 +- .../networknt/schema/MinLengthValidator.java | 5 +- .../schema/MinPropertiesValidator.java | 5 +- .../networknt/schema/MinimumValidator.java | 5 +- .../networknt/schema/MultipleOfValidator.java | 5 +- .../networknt/schema/NotAllowedValidator.java | 5 +- .../com/networknt/schema/NotValidator.java | 7 +- .../com/networknt/schema/OneOfValidator.java | 10 +- .../com/networknt/schema/PatternFormat.java | 27 ++ .../schema/PatternPropertiesValidator.java | 7 +- .../networknt/schema/PatternValidator.java | 6 +- .../networknt/schema/PropertiesValidator.java | 7 +- .../networknt/schema/ReadOnlyValidator.java | 5 +- .../com/networknt/schema/RefValidator.java | 22 +- .../networknt/schema/RequiredValidator.java | 5 +- .../com/networknt/schema/TypeValidator.java | 7 +- .../networknt/schema/UnionTypeValidator.java | 9 +- .../schema/UniqueItemsValidator.java | 5 +- .../networknt/schema/ValidationContext.java | 30 +++ .../networknt/schema/ValidationMessage.java | 33 +++ .../networknt/schema/ValidatorTypeCode.java | 58 +++-- .../schema/url/StandardURLFetcher.java | 16 ++ .../com/networknt/schema/url/URLFetcher.java | 9 + .../schema/BaseJsonSchemaValidatorTest.java | 9 +- .../com/networknt/schema/JsonSchemaTest.java | 8 +- .../PatternPropertiesValidatorTest.java | 3 +- 52 files changed, 895 insertions(+), 328 deletions(-) create mode 100644 src/main/java/com/networknt/schema/AbstractFormat.java create mode 100644 src/main/java/com/networknt/schema/AbstractJsonValidator.java create mode 100644 src/main/java/com/networknt/schema/AbstractKeyword.java create mode 100644 src/main/java/com/networknt/schema/CustomErrorMessageType.java create mode 100644 src/main/java/com/networknt/schema/ErrorMessageType.java create mode 100644 src/main/java/com/networknt/schema/Format.java create mode 100644 src/main/java/com/networknt/schema/FormatKeyword.java create mode 100644 src/main/java/com/networknt/schema/JsonMetaSchema.java create mode 100644 src/main/java/com/networknt/schema/Keyword.java create mode 100644 src/main/java/com/networknt/schema/PatternFormat.java create mode 100644 src/main/java/com/networknt/schema/ValidationContext.java create mode 100644 src/main/java/com/networknt/schema/url/StandardURLFetcher.java create mode 100644 src/main/java/com/networknt/schema/url/URLFetcher.java diff --git a/pom.xml b/pom.xml index ec92daa96..588e7ff32 100644 --- a/pom.xml +++ b/pom.xml @@ -13,9 +13,9 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - + 4.0.0 com.networknt json-schema-validator @@ -23,7 +23,6 @@ A json schema validator that supports draft v4 https://github.com/networknt/json-schema-validator JsonSchemaValidator - stevehu @@ -31,12 +30,10 @@ stevehu@gmail.com - github https://github.com/networknt/json-schema-validator/issues - Apache License Version 2.0 @@ -44,13 +41,11 @@ repo - scm:git://github.com:networknt/json-schema-validator.git scm:git://github.com:networknt/json-schema-validator.git https://github.com:networknt/json-schema-validator.git - ossrh @@ -61,7 +56,6 @@ https://oss.sonatype.org/service/local/staging/deploy/maven2/ - 1.6 1.7 @@ -74,10 +68,7 @@ 2.7.21 1.3 1.4.11.Final - - - com.fasterxml.jackson.core @@ -99,7 +90,6 @@ commons-lang3 ${version.common-lang3} - ch.qos.logback logback-classic @@ -152,7 +142,6 @@ - org.sonatype.plugins @@ -166,13 +155,8 @@ - org.apache.maven.plugins maven-source-plugin @@ -221,7 +205,6 @@ - org.apache.maven.plugins maven-surefire-plugin @@ -234,8 +217,6 @@ - - org.jacoco @@ -261,10 +242,8 @@ - - @@ -274,7 +253,6 @@ - release-sign-artifacts diff --git a/src/main/java/com/networknt/schema/AbstractFormat.java b/src/main/java/com/networknt/schema/AbstractFormat.java new file mode 100644 index 000000000..5a6948d54 --- /dev/null +++ b/src/main/java/com/networknt/schema/AbstractFormat.java @@ -0,0 +1,25 @@ +package com.networknt.schema; + +public abstract class AbstractFormat implements Format{ + private final String name; + private final String errorMessageDescription; + + public AbstractFormat(String name) { + this(name, ""); + } + + public AbstractFormat(String name, String errorMessageDescription) { + this.name = name; + this.errorMessageDescription = errorMessageDescription; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getErrorMessageDescription() { + return errorMessageDescription; + } +} diff --git a/src/main/java/com/networknt/schema/AbstractJsonValidator.java b/src/main/java/com/networknt/schema/AbstractJsonValidator.java new file mode 100644 index 000000000..da3ff49cf --- /dev/null +++ b/src/main/java/com/networknt/schema/AbstractJsonValidator.java @@ -0,0 +1,36 @@ +package com.networknt.schema; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.databind.JsonNode; + +public abstract class AbstractJsonValidator implements JsonValidator { + private final String keyword; + protected AbstractJsonValidator(String keyword) { + this.keyword = keyword; + } + public Set validate(JsonNode node) { + return validate(node, node, AT_ROOT); + } + + protected ValidationMessage buildValidationMessage(ErrorMessageType errorMessageType, String at, String... arguments) { + return ValidationMessage.of(keyword, errorMessageType, at, arguments); + } + protected ValidationMessage buildValidationMessage(ErrorMessageType errorMessageType, String at, Map details) { + return ValidationMessage.of(keyword, errorMessageType, at, details); + } + + protected Set pass() { + return Collections.emptySet(); + } + + protected Set fail(ErrorMessageType errorMessageType, String at, Map details) { + return Collections.singleton(buildValidationMessage(errorMessageType, at, details)); + } + + protected Set fail(ErrorMessageType errorMessageType, String at, String...arguments) { + return Collections.singleton(buildValidationMessage(errorMessageType, at, arguments)); + } +} diff --git a/src/main/java/com/networknt/schema/AbstractKeyword.java b/src/main/java/com/networknt/schema/AbstractKeyword.java new file mode 100644 index 000000000..033140f51 --- /dev/null +++ b/src/main/java/com/networknt/schema/AbstractKeyword.java @@ -0,0 +1,15 @@ +package com.networknt.schema; + + +public abstract class AbstractKeyword implements Keyword { + private final String value; + + public AbstractKeyword(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + +} diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index 9f8dfa0e7..a02b680ca 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -16,14 +16,19 @@ package com.networknt.schema; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import com.fasterxml.jackson.databind.JsonNode; public class AdditionalPropertiesValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(AdditionalPropertiesValidator.class); @@ -34,15 +39,15 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator implements private List patternProperties = new ArrayList(); public AdditionalPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ADDITIONAL_PROPERTIES); + ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ADDITIONAL_PROPERTIES, validationContext); allowAdditionalProperties = false; if (schemaNode.isBoolean()) { allowAdditionalProperties = schemaNode.booleanValue(); } if (schemaNode.isObject()) { allowAdditionalProperties = true; - additionalPropertiesSchema = new JsonSchema(mapper, getValidatorType().getValue(), schemaNode, parentSchema); + additionalPropertiesSchema = new JsonSchema(validationContext, getValidatorType().getValue(), schemaNode, parentSchema); } allowedProperties = new ArrayList(); diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index 790c762f3..da65d0ff9 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,11 +31,11 @@ public class AllOfValidator extends BaseJsonValidator implements JsonValidator { private List schemas = new ArrayList(); - public AllOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ALL_OF); + public AllOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ALL_OF, validationContext); int size = schemaNode.size(); for (int i = 0; i < size; i++) { - schemas.add(new JsonSchema(mapper, getValidatorType().getValue(), schemaNode.get(i), parentSchema)); + schemas.add(new JsonSchema(validationContext, getValidatorType().getValue(), schemaNode.get(i), parentSchema)); } } diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index 611a21613..9ddb671c2 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,11 +31,11 @@ public class AnyOfValidator extends BaseJsonValidator implements JsonValidator { private List schemas = new ArrayList(); - public AnyOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF); + public AnyOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext); int size = schemaNode.size(); for (int i = 0; i < size; i++) { - schemas.add(new JsonSchema(mapper, getValidatorType().getValue(), schemaNode.get(i), parentSchema)); + schemas.add(new JsonSchema(validationContext, getValidatorType().getValue(), schemaNode.get(i), parentSchema)); } } diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 11ec376f1..b863ecc65 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -31,19 +31,16 @@ public abstract class BaseJsonValidator implements JsonValidator { private JsonSchema parentSchema; private JsonSchema subSchema; private ValidatorTypeCode validatorType; - private String errorCode; + private ErrorMessageType errorMessageType; public BaseJsonValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ValidatorTypeCode validatorType) { - this.schemaPath = schemaPath; - this.schemaNode = schemaNode; - this.parentSchema = parentSchema; - this.validatorType = validatorType; - this.subSchema = obainSubSchemaNode(schemaNode); + ValidatorTypeCode validatorType, ValidationContext validationContext) { + this(schemaPath, schemaNode, parentSchema, validatorType, obainSubSchemaNode(schemaNode, validationContext) ); } public BaseJsonValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidatorTypeCode validatorType, JsonSchema subSchema) { + this.errorMessageType = validatorType; this.schemaPath = schemaPath; this.schemaNode = schemaNode; this.parentSchema = parentSchema; @@ -71,20 +68,19 @@ protected boolean hasSubSchema() { return subSchema != null; } - protected JsonSchema obainSubSchemaNode(JsonNode schemaNode){ + protected static JsonSchema obainSubSchemaNode(JsonNode schemaNode, ValidationContext validationContext){ JsonNode node = schemaNode.get("id"); if(node == null) return null; if(node.equals(schemaNode.get("$schema"))) return null; try { - JsonSchemaFactory factory = new JsonSchemaFactory(); String text = node.textValue(); if (text == null) { return null; } else { URL url = URLFactory.toURL(node.textValue()); - return factory.getSchema(url); + return validationContext.getJsonSchemaFactory().getSchema(url); } } catch (MalformedURLException e) { return null; @@ -110,27 +106,16 @@ protected boolean lessThan(double n1, double n2) { protected void parseErrorCode(String errorCodeKey) { JsonNode errorCodeNode = getParentSchema().getSchemaNode().get(errorCodeKey); if (errorCodeNode != null && errorCodeNode.isTextual()) { - errorCode = errorCodeNode.asText(); + String errorCodeText = errorCodeNode.asText(); + if (StringUtils.isNotBlank(errorCodeText)) { + errorMessageType = CustomErrorMessageType.of(errorCodeText); + } } } - private String getErrorCode() { - return errorCode; - } - - private boolean isUsingCustomErrorCode() { - return StringUtils.isNotBlank(errorCode); - } protected ValidationMessage buildValidationMessage(String at, String... arguments) { - ValidationMessage.Builder builder = new ValidationMessage.Builder(); - if (isUsingCustomErrorCode()) { - builder.code(getErrorCode()).path(at).arguments(arguments).type(validatorType.getValue()); - } else { - builder.code(validatorType.getErrorCode()).path(at).arguments(arguments) - .format(validatorType.getMessageFormat()).type(validatorType.getValue()); - } - return builder.build(); + return ValidationMessage.of(getValidatorType().getValue(), errorMessageType, at, arguments); } protected void debug(Logger logger, JsonNode node, JsonNode rootNode, String at) { diff --git a/src/main/java/com/networknt/schema/CustomErrorMessageType.java b/src/main/java/com/networknt/schema/CustomErrorMessageType.java new file mode 100644 index 000000000..4d8fe20ee --- /dev/null +++ b/src/main/java/com/networknt/schema/CustomErrorMessageType.java @@ -0,0 +1,31 @@ +package com.networknt.schema; + +import java.text.MessageFormat; + +public class CustomErrorMessageType implements ErrorMessageType { + private final String errorCode; + private final MessageFormat messageFormat; + + private CustomErrorMessageType(String errorCode, MessageFormat messageFormat) { + this.errorCode = errorCode; + this.messageFormat = messageFormat; + } + + public static ErrorMessageType of(String errorCode) { + return new CustomErrorMessageType(errorCode, null); + } + public static ErrorMessageType of(String errorCode, MessageFormat messageFormat) { + return new CustomErrorMessageType(errorCode, null); + } + + @Override + public String getErrorCode() { + return errorCode; + } + + @Override + public MessageFormat getMessageFormat() { + return messageFormat; + } + +} \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/DependenciesValidator.java b/src/main/java/com/networknt/schema/DependenciesValidator.java index 932e0806b..06a2de92f 100644 --- a/src/main/java/com/networknt/schema/DependenciesValidator.java +++ b/src/main/java/com/networknt/schema/DependenciesValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,9 +27,9 @@ public class DependenciesValidator extends BaseJsonValidator implements JsonVali private final Map> propertyDeps = new HashMap>(); private Map schemaDeps = new HashMap(); - public DependenciesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { + public DependenciesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.DEPENDENCIES); + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.DEPENDENCIES, validationContext); for (Iterator it = schemaNode.fieldNames(); it.hasNext(); ) { String pname = it.next(); @@ -45,7 +44,7 @@ public DependenciesValidator(String schemaPath, JsonNode schemaNode, JsonSchema depsProps.add(pvalue.get(i).asText()); } } else if (pvalue.isObject()) { - schemaDeps.put(pname, new JsonSchema(mapper, pname, pvalue, parentSchema)); + schemaDeps.put(pname, new JsonSchema(validationContext, pname, pvalue, parentSchema)); } } diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java index 473380498..7382ce6c5 100644 --- a/src/main/java/com/networknt/schema/EnumValidator.java +++ b/src/main/java/com/networknt/schema/EnumValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,8 +32,8 @@ public class EnumValidator extends BaseJsonValidator implements JsonValidator { private List nodes; private String error; - public EnumValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ENUM); + public EnumValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ENUM, validationContext); nodes = new ArrayList(); error = "[none]"; diff --git a/src/main/java/com/networknt/schema/ErrorMessageType.java b/src/main/java/com/networknt/schema/ErrorMessageType.java new file mode 100644 index 000000000..7c9d330a7 --- /dev/null +++ b/src/main/java/com/networknt/schema/ErrorMessageType.java @@ -0,0 +1,20 @@ +package com.networknt.schema; + +import java.text.MessageFormat; + +public interface ErrorMessageType { + /** + * Your error code. Please ensure global uniqueness. Builtin error codes are sequential numbers. + * + * Customer error codes could have a prefix to denote the namespace of your custom keywords and errors. + * @return error code + */ + String getErrorCode(); + + /** + * optional message format + * @return the message format or null if no message text shall be rendered. + */ + MessageFormat getMessageFormat(); + +} diff --git a/src/main/java/com/networknt/schema/Format.java b/src/main/java/com/networknt/schema/Format.java new file mode 100644 index 000000000..fa29fa4e1 --- /dev/null +++ b/src/main/java/com/networknt/schema/Format.java @@ -0,0 +1,12 @@ +package com.networknt.schema; + +public interface Format { + /** + * @return the format name as referred to in a json schema format node. + */ + String getName(); + + boolean matches(String value); + + String getErrorMessageDescription(); +} diff --git a/src/main/java/com/networknt/schema/FormatKeyword.java b/src/main/java/com/networknt/schema/FormatKeyword.java new file mode 100644 index 000000000..8e1ffcaac --- /dev/null +++ b/src/main/java/com/networknt/schema/FormatKeyword.java @@ -0,0 +1,40 @@ +package com.networknt.schema; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import com.fasterxml.jackson.databind.JsonNode; + +public class FormatKeyword implements Keyword { + private final ValidatorTypeCode type; + private final Map formats; + + public FormatKeyword(ValidatorTypeCode type, Map formats) { + this.type = type; + this.formats = formats; + } + + Collection getFormats() { + return Collections.unmodifiableCollection(formats.values()); + } + + @Override + public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) + throws Exception { + Format format = null; + if (schemaNode != null && schemaNode.isTextual()) { + String formatName = schemaNode.textValue(); + format = formats.get(formatName); + } + + return new FormatValidator(schemaPath, schemaNode, parentSchema, validationContext, format); + } + + @Override + public String getValue() { + return type.getValue(); + } + + +} diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java index ca9e62053..db35f4db2 100644 --- a/src/main/java/com/networknt/schema/FormatValidator.java +++ b/src/main/java/com/networknt/schema/FormatValidator.java @@ -17,58 +17,22 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashSet; -import java.util.Map; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; public class FormatValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(FormatValidator.class); - private static final Map FORMATS = new HashMap(); - static { - FORMATS.put("date-time", Pattern.compile( - "^\\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}[tT ]\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?([zZ]|[+-]\\d{2}:\\d{2})$")); - FORMATS.put("date", Pattern.compile("^\\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$")); - FORMATS.put("time", Pattern.compile("^\\d{2}:\\d{2}:\\d{2}$")); - FORMATS.put("email", Pattern.compile("^\\S+@\\S+$")); - FORMATS.put("ip-address", Pattern.compile( - "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")); - FORMATS.put("ipv4", Pattern.compile( - "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")); - FORMATS.put("ipv6", Pattern.compile( - "^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$")); - FORMATS.put("uri", Pattern.compile("(^[a-zA-Z][a-zA-Z0-9+-.]*:[^\\s]*$)|(^//[^\\s]*$)")); - FORMATS.put("color", Pattern.compile( - "(#?([0-9A-Fa-f]{3,6})\\b)|(aqua)|(black)|(blue)|(fuchsia)|(gray)|(green)|(lime)|(maroon)|(navy)|(olive)|(orange)|(purple)|(red)|(silver)|(teal)|(white)|(yellow)|(rgb\\(\\s*\\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\b\\s*,\\s*\\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\b\\s*,\\s*\\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\b\\s*\\))|(rgb\\(\\s*(\\d?\\d%|100%)+\\s*,\\s*(\\d?\\d%|100%)+\\s*,\\s*(\\d?\\d%|100%)+\\s*\\))")); - FORMATS.put("hostname", Pattern.compile( - "^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]))*$")); - FORMATS.put("alpha", Pattern.compile("^[a-zA-Z]+$")); - FORMATS.put("alphanumeric", Pattern.compile("^[a-zA-Z0-9]+$")); - FORMATS.put("phone", Pattern.compile("^\\+(?:[0-9] ?){6,14}[0-9]$")); - FORMATS.put("utc-millisec", Pattern.compile("^[0-9]+(\\.?[0-9]+)?$")); - FORMATS.put("style", Pattern.compile("\\s*(.+?):\\s*([^;]+);?")); - } - - private String format; - private Pattern p; - - public FormatValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.FORMAT); - format = ""; - if (schemaNode != null && schemaNode.isTextual()) { - format = schemaNode.textValue(); - p = FORMATS.get(format); - } + private final Format format; + public FormatValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, Format format) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.FORMAT, validationContext); + this.format = format; parseErrorCode(getValidatorType().getErrorCodeKey()); } @@ -82,15 +46,14 @@ public Set validate(JsonNode node, JsonNode rootNode, String return errors; } - if (p != null) { + if (format != null) { try { - Matcher m = p.matcher(node.textValue()); - if (!m.matches()) { - errors.add(buildValidationMessage(at, format, p.pattern())); + if (!format.matches(node.textValue())) { + errors.add(buildValidationMessage(at, format.getName(), format.getErrorMessageDescription())); } } catch (PatternSyntaxException pse) { // String is considered valid if pattern is invalid - logger.error("Failed to apply pattern on " + at + ": Invalid RE syntax [" + format + "]", pse); + logger.error("Failed to apply pattern on " + at + ": Invalid RE syntax [" + format.getName() + "]", pse); } } diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 38cbdcdba..98188026b 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,14 +35,14 @@ public class ItemsValidator extends BaseJsonValidator implements JsonValidator { private boolean additionalItems = true; private JsonSchema additionalSchema; - public ItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS); + public ItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS, validationContext); if (schemaNode.isObject()) { - schema = new JsonSchema(mapper, getValidatorType().getValue(), schemaNode, parentSchema); + schema = new JsonSchema(validationContext, getValidatorType().getValue(), schemaNode, parentSchema); } else { tupleSchema = new ArrayList(); for (JsonNode s : schemaNode) { - tupleSchema.add(new JsonSchema(mapper, getValidatorType().getValue(), s, parentSchema)); + tupleSchema.add(new JsonSchema(validationContext, getValidatorType().getValue(), s, parentSchema)); } JsonNode addItemNode = getParentSchema().getSchemaNode().get(PROPERTY_ADDITIONAL_ITEMS); @@ -51,7 +50,7 @@ public ItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentS if (addItemNode.isBoolean()) { additionalItems = addItemNode.asBoolean(); } else if (addItemNode.isObject()) { - additionalSchema = new JsonSchema(mapper, addItemNode); + additionalSchema = new JsonSchema(validationContext, addItemNode); } } } diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java new file mode 100644 index 000000000..1855cfe14 --- /dev/null +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2016 Network New Technologies Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.networknt.schema; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; + +public class JsonMetaSchema { + + + private static final Logger logger = LoggerFactory + .getLogger(JsonMetaSchema.class); + + + private static class DraftV4 { + private static String URI = "http://json-schema.org/draft-04/schema#"; + // Draft 6 uses "$id" + private static final String DRAFT_4_ID = "id"; + public static final List BUILTIN_FORMATS = new ArrayList(); + static { + BUILTIN_FORMATS.add(pattern("date-time", + "^\\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}[tT ]\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?([zZ]|[+-]\\d{2}:\\d{2})$")); + BUILTIN_FORMATS.add(pattern("date", "^\\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}$")); + BUILTIN_FORMATS.add(pattern("time", "^\\d{2}:\\d{2}:\\d{2}$")); + BUILTIN_FORMATS.add(pattern("email", "^\\S+@\\S+$")); + BUILTIN_FORMATS.add(pattern("ip-address", + "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")); + BUILTIN_FORMATS.add(pattern("ipv4", + "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")); + BUILTIN_FORMATS.add(pattern("ipv6", + "^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$")); + BUILTIN_FORMATS.add(pattern("uri", "(^[a-zA-Z][a-zA-Z0-9+-.]*:[^\\s]*$)|(^//[^\\s]*$)")); + BUILTIN_FORMATS.add(pattern("color", + "(#?([0-9A-Fa-f]{3,6})\\b)|(aqua)|(black)|(blue)|(fuchsia)|(gray)|(green)|(lime)|(maroon)|(navy)|(olive)|(orange)|(purple)|(red)|(silver)|(teal)|(white)|(yellow)|(rgb\\(\\s*\\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\b\\s*,\\s*\\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\b\\s*,\\s*\\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\b\\s*\\))|(rgb\\(\\s*(\\d?\\d%|100%)+\\s*,\\s*(\\d?\\d%|100%)+\\s*,\\s*(\\d?\\d%|100%)+\\s*\\))")); + BUILTIN_FORMATS.add(pattern("hostname", + "^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]))*$")); + BUILTIN_FORMATS.add(pattern("alpha", "^[a-zA-Z]+$")); + BUILTIN_FORMATS.add(pattern("alphanumeric", "^[a-zA-Z0-9]+$")); + BUILTIN_FORMATS.add(pattern("phone", "^\\+(?:[0-9] ?){6,14}[0-9]$")); + BUILTIN_FORMATS.add(pattern("utc-millisec", "^[0-9]+(\\.?[0-9]+)?$")); + BUILTIN_FORMATS.add(pattern("style", "\\s*(.+?):\\s*([^;]+);?")); + } + + static PatternFormat pattern(String name, String regex) { + return new PatternFormat(name, regex); + } + + public static JsonMetaSchema getInstance() { + return new Builder(URI) + .idKeyword(DRAFT_4_ID) + .addFormats(BUILTIN_FORMATS) + .addKeywords(ValidatorTypeCode.getNonFormatKeywords()) + .build(); + } + } + + public static class Builder { + private Map keywords = new HashMap(); + private Map formats = new HashMap(); + private String uri; + private String idKeyword = "id"; + + public Builder(String uri) { + this.uri = uri; + } + + private static Map createKeywordsMap(Map kwords, Map formats) { + final Map map = new HashMap(); + for (Map.Entry type: kwords.entrySet()) { + String keywordName = type.getKey(); + Keyword keyword = type.getValue(); + if (ValidatorTypeCode.FORMAT.getValue().equals(keywordName)) { + if (!(keyword instanceof FormatKeyword)) { + throw new IllegalArgumentException("Overriding the keyword 'format' is not supported"); + } + // ignore - format keyword will be created again below. + } else { + map.put(keyword.getValue(), keyword); + } + } + final FormatKeyword formatKeyword = new FormatKeyword(ValidatorTypeCode.FORMAT, formats); + map.put(formatKeyword.getValue(), formatKeyword); + return Collections.unmodifiableMap(map); + } + + public Builder addKeyword(Keyword keyword) { + this.keywords.put(keyword.getValue(), keyword); + return this; + } + + public Builder addKeywords(Collection keywords) { + for (Keyword keyword: keywords) { + this.keywords.put(keyword.getValue(), keyword); + } + return this; + } + + public Builder addFormat(Format format) { + this.formats.put(format.getName(), format); + return this; + } + + public Builder addFormats(Collection formats) { + for (Format format : formats) { + addFormat(format); + } + return this; + } + + + public Builder idKeyword(String idKeyword) { + this.idKeyword = idKeyword; + return this; + } + + public JsonMetaSchema build() { + // create builtin keywords with (custom) formats. + final Map kwords = createKeywordsMap(keywords, formats); + return new JsonMetaSchema(uri, idKeyword, kwords); + } + } + + private final String uri; + private final String idKeyword; + private final Map keywords; + + private JsonMetaSchema(String uri, String idKeyword, Map keywords) { + if (StringUtils.isBlank(uri)) { + throw new IllegalArgumentException("uri must not be null or blank"); + } + if (StringUtils.isBlank(idKeyword)) { + throw new IllegalArgumentException("idKeyword must not be null or blank"); + } + if (keywords == null) { + throw new IllegalArgumentException("keywords must not be null "); + } + + this.uri = uri; + this.idKeyword = idKeyword; + this.keywords = keywords; + } + + public static JsonMetaSchema getDraftV4() { + return DraftV4.getInstance(); + } + + /** + * Builder without keywords or formats. + * + * Use {@link #getDraftV4()} for the Draft 4 Metaschema, or if you need a builder based on Draft4, use + * + * + * JsonMetaSchema.builder("http://your-metaschema-uri", JsonSchemaFactory.getDraftV4()).build(); + * + * + * @param uri the URI of the metaschema that will be defined via this builder. + * @return a builder instance without any keywords or formats - usually not what one needs. + */ + public static Builder builder(String uri) { + return new Builder(uri); + } + + /** + * + * @param uri the URI of your new JsonMetaSchema that will be defined via this builder. + * @param blueprint the JsonMetaSchema to base your custom JsonMetaSchema on. + * @return a builder instance preconfigured to be the same as blueprint, but with a different uri. + */ + public static Builder builder(String uri, JsonMetaSchema blueprint) { + FormatKeyword formatKeyword = (FormatKeyword)blueprint.keywords.get(ValidatorTypeCode.FORMAT.getValue()); + if (formatKeyword == null) { + throw new IllegalArgumentException("The formatKeyword did not exist - blueprint is invalid."); + } + return builder(uri) + .idKeyword(blueprint.idKeyword) + .addKeywords(blueprint.keywords.values()) + .addFormats(formatKeyword.getFormats()); + } + + public String readId(JsonNode schemaNode) { + JsonNode idNode = schemaNode.get(idKeyword); + if (idNode == null || !idNode.isTextual()) { + return null; + } + return idNode.textValue(); + } + + public String getUri() { + return uri; + } + + public Optional newValidator(ValidationContext validationContext, String schemaPath, String keyword /* keyword */, JsonNode schemaNode, + JsonSchema parentSchema) { + + try { + Keyword kw = keywords.get(keyword); + if (kw == null) { + logger.warn("Unknown keyword " + keyword); + return Optional.empty(); + } + return Optional.of(kw.newValidator(schemaPath, schemaNode, parentSchema, validationContext)); + } catch (InvocationTargetException e) { + if (e.getTargetException() instanceof JsonSchemaException) { + throw (JsonSchemaException) e.getTargetException(); + } else { + logger.warn("Could not load validator " + keyword); + throw new JsonSchemaException(e.getTargetException()); + } + } catch(JsonSchemaException e) { + throw e; + } catch (Exception e) { + logger.warn("Could not load validator " + keyword); + throw new JsonSchemaException(e); + } + } + + +} diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 9447b0c43..4d6876824 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -16,54 +16,48 @@ package com.networknt.schema; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.UnsupportedEncodingException; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.net.URLDecoder; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.fasterxml.jackson.databind.JsonNode; + /** * This is the core of json constraint implementation. It parses json constraint * file and generates JsonValidators. The class is thread safe, once it is * constructed, it can be used to validate multiple json data concurrently. */ public class JsonSchema extends BaseJsonValidator { - private static final Logger logger = LoggerFactory.getLogger(JsonSchema.class); private static final Pattern intPattern = Pattern.compile("^[0-9]+$"); - protected Map validators; - private ObjectMapper mapper; + protected final Map validators; + private final ValidationContext validationContext; - JsonSchema(ObjectMapper mapper, JsonNode schemaNode) { - this(mapper, "#", schemaNode, null); + JsonSchema(ValidationContext validationContext, JsonNode schemaNode) { + this(validationContext, "#", schemaNode, null); } - JsonSchema(ObjectMapper mapper, String schemaPath, JsonNode schemaNode, + JsonSchema(ValidationContext validationContext, String schemaPath, JsonNode schemaNode, JsonSchema parent) { - super(schemaPath, schemaNode, parent, null); - this.init(mapper, schemaNode); + this(validationContext, schemaPath, schemaNode, parent, obainSubSchemaNode(schemaNode, validationContext)); } - JsonSchema(ObjectMapper mapper, String schemaPath, JsonNode schemaNode, + JsonSchema(ValidationContext validatorFactory, String schemaPath, JsonNode schemaNode, JsonSchema parent, JsonSchema subSchema) { super(schemaPath, schemaNode, parent, null, subSchema); - this.init(mapper, schemaNode); + this.validationContext = validatorFactory; + this.validators = Collections.unmodifiableMap(this.read(schemaNode)); } - public JsonSchema(ObjectMapper mapper, JsonNode schemaNode, JsonSchema subSchema) { - this(mapper, "#", schemaNode, null, subSchema); - } - - private void init(ObjectMapper mapper, JsonNode schemaNode) { - this.mapper = mapper; - this.validators = new LinkedHashMap(); - this.read(schemaNode); + public JsonSchema(ValidationContext validationContext, JsonNode schemaNode, JsonSchema subSchema) { + this(validationContext, "#", schemaNode, null, subSchema); } /** @@ -109,50 +103,26 @@ public JsonSchema findAncestor() { return ancestor; } - @SuppressWarnings("unchecked") - private void read(JsonNode schemaNode) { + private Map read(JsonNode schemaNode) { + Map validators = new HashMap(); Iterator pnames = schemaNode.fieldNames(); while (pnames.hasNext()) { String pname = pnames.next(); JsonNode n = schemaNode.get(pname); - String shortClassName = pname; - if (shortClassName.startsWith("$")) { - // remove "$" from class name for $ref schema - shortClassName = shortClassName.substring(1); + Optional validator = validationContext.newValidator(getSchemaPath(), pname, n, this); + if (validator.isPresent()) { + validators.put(getSchemaPath() + "/" + pname, validator.get()); } - try { - ValidatorTypeCode.fromValue(shortClassName); - - String className = Character.toUpperCase(shortClassName.charAt(0)) - + shortClassName.substring(1) + "Validator"; - Class clazz = (Class) Class - .forName("com.networknt.schema." + className); - Constructor c = null; - c = clazz.getConstructor(new Class[]{String.class, - JsonNode.class, JsonSchema.class, ObjectMapper.class}); - validators.put(getSchemaPath() + "/" + pname, c.newInstance( - getSchemaPath() + "/" + pname, n, this, mapper)); - } catch (IllegalArgumentException e) { - // ignore unsupported schema node - } catch (InvocationTargetException e) { - if (e.getTargetException() instanceof JsonSchemaException) { - throw (JsonSchemaException) e.getTargetException(); - } else { - logger.info("Could not load validator " + pname); - } - } catch (Exception e) { - logger.info("Could not load validator " + pname); - } } + return validators; } - public Set validate(JsonNode JsonNode, - JsonNode rootNode, String at) { - Set errors = new HashSet(); + public Set validate(JsonNode jsonNode, JsonNode rootNode, String at) { + Set errors = new LinkedHashSet(); for (JsonValidator v : validators.values()) { - errors.addAll(v.validate(JsonNode, rootNode, at)); + errors.addAll(v.validate(jsonNode, rootNode, at)); } return errors; } diff --git a/src/main/java/com/networknt/schema/JsonSchemaException.java b/src/main/java/com/networknt/schema/JsonSchemaException.java index 7a48a1f3e..5c1d744c4 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaException.java +++ b/src/main/java/com/networknt/schema/JsonSchemaException.java @@ -19,6 +19,10 @@ public class JsonSchemaException extends RuntimeException { private static final long serialVersionUID = -7805792737596582110L; + public JsonSchemaException(ValidationMessage validationMessage) { + super(validationMessage.getMessage()); + } + public JsonSchemaException(String message) { super(message); } diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 3e53e3613..c3deef8c6 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -16,36 +16,153 @@ package com.networknt.schema; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; -public class JsonSchemaFactory { +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.url.StandardURLFetcher; +import com.networknt.schema.url.URLFetcher; - // Draft 6 uses "$id" - private static final String DRAFT_4_ID = "id"; +public class JsonSchemaFactory { private static final Logger logger = LoggerFactory .getLogger(JsonSchemaFactory.class); - private ObjectMapper mapper; + + + public static class Builder { + private ObjectMapper objectMapper = new ObjectMapper(); + private URLFetcher urlFetcher; + private String defaultMetaSchemaURI; + private Map jsonMetaSchemas = new HashMap(); + + public Builder objectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + public Builder urlFetcher(URLFetcher urlFetcher) { + this.urlFetcher = urlFetcher; + return this; + } + + public Builder defaultMetaSchemaURI(String defaultMetaSchemaURI) { + this.defaultMetaSchemaURI = defaultMetaSchemaURI; + return this; + } + + public Builder addMetaSchema(JsonMetaSchema jsonMetaSchema) { + this.jsonMetaSchemas.put(jsonMetaSchema.getUri(), jsonMetaSchema); + return this; + } + + public Builder addMetaSchemas(Collection jsonMetaSchemas) { + for (JsonMetaSchema jsonMetaSchema: jsonMetaSchemas) { + this.jsonMetaSchemas.put(jsonMetaSchema.getUri(), jsonMetaSchema); + } + return this; + } + + public JsonSchemaFactory build() { + // create builtin keywords with (custom) formats. + return new JsonSchemaFactory( + objectMapper == null ? new ObjectMapper() : objectMapper, + urlFetcher == null ? new StandardURLFetcher(): urlFetcher, + defaultMetaSchemaURI, + jsonMetaSchemas + ); + } + } + + private final ObjectMapper mapper; + private final URLFetcher urlFetcher; + private final String defaultMetaSchemaURI; + private final Map jsonMetaSchemas; + + private JsonSchemaFactory(ObjectMapper mapper, URLFetcher urlFetcher, String defaultMetaSchemaURI, Map jsonMetaSchemas) { + if (mapper == null) { + throw new IllegalArgumentException("ObjectMapper must not be null"); + } + if (urlFetcher == null) { + throw new IllegalArgumentException("URLFetcher must not be null"); + } + if (defaultMetaSchemaURI == null || defaultMetaSchemaURI.trim().isEmpty()) { + throw new IllegalArgumentException("defaultMetaSchemaURI must not be null or empty"); + } + if (jsonMetaSchemas == null || jsonMetaSchemas.isEmpty()) { + throw new IllegalArgumentException("Json Meta Schemas must not be null or empty"); + } + if (jsonMetaSchemas.get(defaultMetaSchemaURI) == null) { + throw new IllegalArgumentException("Meta Schema for default Meta Schema URI must be provided"); + } + this.mapper = mapper; + this.defaultMetaSchemaURI = defaultMetaSchemaURI; + this.urlFetcher = urlFetcher; + this.jsonMetaSchemas = jsonMetaSchemas; + } - public JsonSchemaFactory() { - this(new ObjectMapper()); + /** + * Builder without keywords or formats. + * + * Use {@link #getDraftV4()} instead, or if you need a builder based on Draft4, use + * + * + * JsonSchemaFactory.builder(JsonSchemaFactory.getDraftV4()).build(); + * + * + * @return a builder instance without any keywords or formats - usually not what one needs. + */ + static Builder builder() { + return new Builder(); + } + + public static JsonSchemaFactory getInstance() { + JsonMetaSchema draftV4 = JsonMetaSchema.getDraftV4(); + return builder() + .defaultMetaSchemaURI(draftV4.getUri()) + .addMetaSchema(draftV4) + .build(); + } + + public static Builder builder(JsonSchemaFactory blueprint) { + return builder() + .addMetaSchemas(blueprint.jsonMetaSchemas.values()) + .urlFetcher(blueprint.urlFetcher) + .defaultMetaSchemaURI(blueprint.defaultMetaSchemaURI) + .objectMapper(blueprint.mapper); + } + + private JsonSchema newJsonSchema(JsonNode schemaNode) { + final ValidationContext validationContext = createValidationContext(schemaNode); + return new JsonSchema(validationContext, schemaNode); } - public JsonSchemaFactory(ObjectMapper mapper) { - this.mapper = mapper; + protected ValidationContext createValidationContext(JsonNode schemaNode) { + final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); + return new ValidationContext(jsonMetaSchema, this); } + private JsonMetaSchema findMetaSchemaForSchema(JsonNode schemaNode) { + final JsonNode uriNode = schemaNode.get("$schema"); + final String uri = uriNode == null || uriNode.isNull() ? defaultMetaSchemaURI: uriNode.textValue(); + final JsonMetaSchema jsonMetaSchema = jsonMetaSchemas.get(uri); + if (jsonMetaSchema == null) { + throw new JsonSchemaException("Unknown Metaschema: " + uri); + } + return jsonMetaSchema; + } + public JsonSchema getSchema(String schema) { try { - JsonNode schemaNode = mapper.readTree(schema); - return new JsonSchema(mapper, schemaNode); + final JsonNode schemaNode = mapper.readTree(schema); + return newJsonSchema(schemaNode); } catch (IOException ioe) { logger.error("Failed to load json schema!", ioe); throw new JsonSchemaException(ioe); @@ -54,8 +171,8 @@ public JsonSchema getSchema(String schema) { public JsonSchema getSchema(InputStream schemaStream) { try { - JsonNode schemaNode = mapper.readTree(schemaStream); - return new JsonSchema(mapper, schemaNode); + final JsonNode schemaNode = mapper.readTree(schemaStream); + return newJsonSchema(schemaNode); } catch (IOException ioe) { logger.error("Failed to load json schema!", ioe); throw new JsonSchemaException(ioe); @@ -64,15 +181,23 @@ public JsonSchema getSchema(InputStream schemaStream) { public JsonSchema getSchema(URL schemaURL) { try { - - JsonNode schemaNode = mapper.readTree(schemaURL.openStream()); - - if (this.idMatchesSourceUrl(schemaNode, schemaURL)) { - return new JsonSchema(mapper, schemaNode, null); + InputStream inputStream = null; + try { + inputStream = urlFetcher.fetch(schemaURL); + JsonNode schemaNode = mapper.readTree(inputStream); + final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); + + if (idMatchesSourceUrl(jsonMetaSchema, schemaNode, schemaURL)) { + + return new JsonSchema(new ValidationContext(jsonMetaSchema, this), schemaNode, null); + } + + return newJsonSchema(schemaNode); + } finally { + if (inputStream != null) { + inputStream.close(); + } } - - return new JsonSchema(mapper, schemaNode); - } catch (IOException ioe) { logger.error("Failed to load json schema!", ioe); throw new JsonSchemaException(ioe); @@ -80,18 +205,15 @@ public JsonSchema getSchema(URL schemaURL) { } public JsonSchema getSchema(JsonNode jsonNode) { - return new JsonSchema(mapper, jsonNode); + return newJsonSchema(jsonNode); } - private boolean idMatchesSourceUrl(JsonNode schema, URL schemaUrl) { + private boolean idMatchesSourceUrl(JsonMetaSchema metaSchema, JsonNode schema, URL schemaUrl) { - JsonNode idNode = schema.get(DRAFT_4_ID); - - if (idNode == null) { + String id = metaSchema.readId(schema); + if (id == null || id.isEmpty()) { return false; } - - String id = idNode.asText(); logger.info("Matching " + id + " to " + schemaUrl.toString()); return id.equals(schemaUrl.toString()); diff --git a/src/main/java/com/networknt/schema/JsonValidator.java b/src/main/java/com/networknt/schema/JsonValidator.java index 0360bfea8..5667bbe7a 100644 --- a/src/main/java/com/networknt/schema/JsonValidator.java +++ b/src/main/java/com/networknt/schema/JsonValidator.java @@ -25,6 +25,7 @@ */ public interface JsonValidator { public static final String AT_ROOT = "$"; + /** * Validate the given root JsonNode, starting at the root of the data path. diff --git a/src/main/java/com/networknt/schema/Keyword.java b/src/main/java/com/networknt/schema/Keyword.java new file mode 100644 index 000000000..7f925faa6 --- /dev/null +++ b/src/main/java/com/networknt/schema/Keyword.java @@ -0,0 +1,10 @@ +package com.networknt.schema; + + +import com.fasterxml.jackson.databind.JsonNode; + +public interface Keyword { + String getValue(); + + JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception; +} diff --git a/src/main/java/com/networknt/schema/MaxItemsValidator.java b/src/main/java/com/networknt/schema/MaxItemsValidator.java index b484f6258..bd2461cce 100644 --- a/src/main/java/com/networknt/schema/MaxItemsValidator.java +++ b/src/main/java/com/networknt/schema/MaxItemsValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,8 +28,8 @@ public class MaxItemsValidator extends BaseJsonValidator implements JsonValidato private int max = 0; - public MaxItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_ITEMS); + public MaxItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_ITEMS, validationContext); if (schemaNode.isIntegralNumber()) { max = schemaNode.intValue(); } diff --git a/src/main/java/com/networknt/schema/MaxLengthValidator.java b/src/main/java/com/networknt/schema/MaxLengthValidator.java index 858a4a911..06bce9b95 100644 --- a/src/main/java/com/networknt/schema/MaxLengthValidator.java +++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,8 +28,8 @@ public class MaxLengthValidator extends BaseJsonValidator implements JsonValidat private int maxLength; - public MaxLengthValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_LENGTH); + public MaxLengthValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_LENGTH, validationContext); maxLength = Integer.MAX_VALUE; if (schemaNode != null && schemaNode.isIntegralNumber()) { maxLength = schemaNode.intValue(); diff --git a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java index 89b2ad734..ef7a2d5ea 100644 --- a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,8 +29,8 @@ public class MaxPropertiesValidator extends BaseJsonValidator implements JsonVal private int max; public MaxPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_PROPERTIES); + ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_PROPERTIES, validationContext); if (schemaNode.isIntegralNumber()) { max = schemaNode.intValue(); } diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java index 02c15df70..10f67a61a 100644 --- a/src/main/java/com/networknt/schema/MaximumValidator.java +++ b/src/main/java/com/networknt/schema/MaximumValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,9 +30,9 @@ public class MaximumValidator extends BaseJsonValidator implements JsonValidator private double maximum; private boolean excludeEqual = false; - public MaximumValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { + public MaximumValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAXIMUM); + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAXIMUM, validationContext); if (schemaNode.isNumber()) { maximum = schemaNode.doubleValue(); } else { diff --git a/src/main/java/com/networknt/schema/MinItemsValidator.java b/src/main/java/com/networknt/schema/MinItemsValidator.java index 8c81dc846..050794ec4 100644 --- a/src/main/java/com/networknt/schema/MinItemsValidator.java +++ b/src/main/java/com/networknt/schema/MinItemsValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,8 +28,8 @@ public class MinItemsValidator extends BaseJsonValidator implements JsonValidato private int min = 0; - public MinItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_ITEMS); + public MinItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_ITEMS, validationContext); if (schemaNode.isIntegralNumber()) { min = schemaNode.intValue(); } diff --git a/src/main/java/com/networknt/schema/MinLengthValidator.java b/src/main/java/com/networknt/schema/MinLengthValidator.java index c9ec3e7ae..56f9dbc63 100644 --- a/src/main/java/com/networknt/schema/MinLengthValidator.java +++ b/src/main/java/com/networknt/schema/MinLengthValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,8 +28,8 @@ public class MinLengthValidator extends BaseJsonValidator implements JsonValidat private int minLength; - public MinLengthValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_LENGTH); + public MinLengthValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_LENGTH, validationContext); minLength = Integer.MIN_VALUE; if (schemaNode != null && schemaNode.isIntegralNumber()) { minLength = schemaNode.intValue(); diff --git a/src/main/java/com/networknt/schema/MinPropertiesValidator.java b/src/main/java/com/networknt/schema/MinPropertiesValidator.java index ce4b40087..076bf1f00 100644 --- a/src/main/java/com/networknt/schema/MinPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MinPropertiesValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,8 +29,8 @@ public class MinPropertiesValidator extends BaseJsonValidator implements JsonVal protected int min; public MinPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_PROPERTIES); + ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_PROPERTIES, validationContext); if (schemaNode.isIntegralNumber()) { min = schemaNode.intValue(); } diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java index 3cb360bff..3e5107b8e 100644 --- a/src/main/java/com/networknt/schema/MinimumValidator.java +++ b/src/main/java/com/networknt/schema/MinimumValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,8 +30,8 @@ public class MinimumValidator extends BaseJsonValidator implements JsonValidator private double minimum; private boolean excluded = false; - public MinimumValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MINIMUM); + public MinimumValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MINIMUM, validationContext); if (schemaNode.isNumber()) { minimum = schemaNode.doubleValue(); } else { diff --git a/src/main/java/com/networknt/schema/MultipleOfValidator.java b/src/main/java/com/networknt/schema/MultipleOfValidator.java index f214be1c7..b91859a54 100644 --- a/src/main/java/com/networknt/schema/MultipleOfValidator.java +++ b/src/main/java/com/networknt/schema/MultipleOfValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,9 +28,9 @@ public class MultipleOfValidator extends BaseJsonValidator implements JsonValida private double divisor = 0; - public MultipleOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { + public MultipleOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MULTIPLE_OF); + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MULTIPLE_OF, validationContext); if (schemaNode.isNumber()) { divisor = schemaNode.doubleValue(); } diff --git a/src/main/java/com/networknt/schema/NotAllowedValidator.java b/src/main/java/com/networknt/schema/NotAllowedValidator.java index ee9f7f44c..1bfa7f48d 100644 --- a/src/main/java/com/networknt/schema/NotAllowedValidator.java +++ b/src/main/java/com/networknt/schema/NotAllowedValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,9 +31,9 @@ public class NotAllowedValidator extends BaseJsonValidator implements JsonValida private List fieldNames = new ArrayList(); - public NotAllowedValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { + public NotAllowedValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.NOT_ALLOWED); + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.NOT_ALLOWED, validationContext); if (schemaNode.isArray()) { int size = schemaNode.size(); for (int i = 0; i < size; i++) { diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index b0ee57ec1..87aaf212e 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,9 +28,9 @@ public class NotValidator extends BaseJsonValidator implements JsonValidator { private JsonSchema schema; - public NotValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.NOT); - schema = new JsonSchema(mapper, getValidatorType().getValue(), schemaNode, parentSchema); + public NotValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.NOT, validationContext); + schema = new JsonSchema(validationContext, getValidatorType().getValue(), schemaNode, parentSchema); parseErrorCode(getValidatorType().getErrorCodeKey()); } diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index b8f39cf76..65f530874 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,11 +33,11 @@ public class OneOfValidator extends BaseJsonValidator implements JsonValidator { private List schemas = new ArrayList(); - public OneOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ONE_OF); + public OneOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ONE_OF, validationContext); int size = schemaNode.size(); for (int i = 0; i < size; i++) { - schemas.add(new JsonSchema(mapper, getValidatorType().getValue(), schemaNode.get(i), parentSchema)); + schemas.add(new JsonSchema(validationContext, getValidatorType().getValue(), schemaNode.get(i), parentSchema)); } parseErrorCode(getValidatorType().getErrorCodeKey()); @@ -68,8 +67,7 @@ public Set validate(JsonNode node, JsonNode rootNode, String for (Iterator it = errors.iterator(); it.hasNext();) { ValidationMessage msg = it.next(); - if (ValidatorTypeCode.ADDITIONAL_PROPERTIES.equals(ValidatorTypeCode.fromValue(msg - .getType()))) { + if (ValidatorTypeCode.ADDITIONAL_PROPERTIES.getValue().equals(msg.getType())) { it.remove(); } } diff --git a/src/main/java/com/networknt/schema/PatternFormat.java b/src/main/java/com/networknt/schema/PatternFormat.java new file mode 100644 index 000000000..bc5df8d07 --- /dev/null +++ b/src/main/java/com/networknt/schema/PatternFormat.java @@ -0,0 +1,27 @@ +package com.networknt.schema; + +import java.util.regex.Pattern; + +public class PatternFormat implements Format { + private final String name; + private final Pattern pattern; + + public PatternFormat(String name, String regex) { + this.name = name; + this.pattern = Pattern.compile(regex); + } + + @Override + public boolean matches(String value) { + return pattern.matcher(value).matches(); + } + + @Override + public String getName() { + return name; + } + @Override + public String getErrorMessageDescription() { + return pattern.pattern(); + } +} \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java index b169f7ef6..905a2a54f 100644 --- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,15 +30,15 @@ public class PatternPropertiesValidator extends BaseJsonValidator implements Jso private Map schemas = new IdentityHashMap(); public PatternPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PATTERN_PROPERTIES); + ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PATTERN_PROPERTIES, validationContext); if (!schemaNode.isObject()) { throw new JsonSchemaException("patternProperties must be an object node"); } Iterator names = schemaNode.fieldNames(); while (names.hasNext()) { String name = names.next(); - schemas.put(Pattern.compile(name), new JsonSchema(mapper, name, schemaNode.get(name), parentSchema)); + schemas.put(Pattern.compile(name), new JsonSchema(validationContext, name, schemaNode.get(name), parentSchema)); } } diff --git a/src/main/java/com/networknt/schema/PatternValidator.java b/src/main/java/com/networknt/schema/PatternValidator.java index d2a9f988f..096b43ff8 100644 --- a/src/main/java/com/networknt/schema/PatternValidator.java +++ b/src/main/java/com/networknt/schema/PatternValidator.java @@ -17,12 +17,10 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collections; -import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -34,9 +32,9 @@ public class PatternValidator extends BaseJsonValidator implements JsonValidator private String pattern; private Pattern p; - public PatternValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { + public PatternValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PATTERN); + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PATTERN, validationContext); pattern = ""; if (schemaNode != null && schemaNode.isTextual()) { pattern = schemaNode.textValue(); diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index b7e117307..4c68641cc 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,12 +27,12 @@ public class PropertiesValidator extends BaseJsonValidator implements JsonValida private static final Logger logger = LoggerFactory.getLogger(PropertiesValidator.class); private Map schemas; - public PropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PROPERTIES); + public PropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PROPERTIES, validationContext); schemas = new HashMap(); for (Iterator it = schemaNode.fieldNames(); it.hasNext(); ) { String pname = it.next(); - schemas.put(pname, new JsonSchema(mapper, schemaPath + "/" + pname, schemaNode.get(pname), parentSchema)); + schemas.put(pname, new JsonSchema(validationContext, schemaPath + "/" + pname, schemaNode.get(pname), parentSchema)); } } diff --git a/src/main/java/com/networknt/schema/ReadOnlyValidator.java b/src/main/java/com/networknt/schema/ReadOnlyValidator.java index def91c363..3d8ea2c49 100644 --- a/src/main/java/com/networknt/schema/ReadOnlyValidator.java +++ b/src/main/java/com/networknt/schema/ReadOnlyValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,8 +31,8 @@ public class ReadOnlyValidator extends BaseJsonValidator implements JsonValidato private List fieldNames = new ArrayList(); - public ReadOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.READ_ONLY); + public ReadOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.READ_ONLY, validationContext); if (schemaNode.isArray()) { int size = schemaNode.size(); for (int i = 0; i < size; i++) { diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index c6bb7a588..6fae589a8 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.url.URLFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,28 +36,27 @@ public class RefValidator extends BaseJsonValidator implements JsonValidator { private final String REF_CURRENT = "#"; private final String REF_RELATIVE = "../"; - public RefValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { + public RefValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.REF); + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.REF, validationContext); String refValue = schemaNode.asText(); if (!refValue.startsWith(REF_CURRENT)) { // handle remote ref - String schemaUrl = refValue; - int index = refValue.indexOf(REF_CURRENT); + String schemaUrl = refValue; + int index = refValue.indexOf(REF_CURRENT); if (index > 0) { schemaUrl = schemaUrl.substring(0, index); } - if(isRelativePath(schemaUrl)){ - schemaUrl = obtainAbsolutePath(parentSchema, schemaUrl); - } + if(isRelativePath(schemaUrl)){ + schemaUrl = obtainAbsolutePath(parentSchema, schemaUrl); + } - JsonSchemaFactory factory = new JsonSchemaFactory(mapper); try { URL url = URLFactory.toURL(schemaUrl); - parentSchema = factory.getSchema(url); + parentSchema = validationContext.getJsonSchemaFactory().getSchema(url); } catch (MalformedURLException e) { InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(schemaUrl); - parentSchema = factory.getSchema(is); + parentSchema = validationContext.getJsonSchemaFactory().getSchema(is); } if (index < 0) { schema = parentSchema.findAncestor(); @@ -71,7 +69,7 @@ public RefValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSch } else { JsonNode node = parentSchema.getRefSchemaNode(refValue); if (node != null) { - schema = new JsonSchema(mapper, refValue, node, parentSchema); + schema = new JsonSchema(validationContext, refValue, node, parentSchema); } } } diff --git a/src/main/java/com/networknt/schema/RequiredValidator.java b/src/main/java/com/networknt/schema/RequiredValidator.java index 96185cacf..e77e7e5ce 100644 --- a/src/main/java/com/networknt/schema/RequiredValidator.java +++ b/src/main/java/com/networknt/schema/RequiredValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,9 +31,9 @@ public class RequiredValidator extends BaseJsonValidator implements JsonValidato private List fieldNames = new ArrayList(); - public RequiredValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { + public RequiredValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.REQUIRED); + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.REQUIRED, validationContext); if (schemaNode.isArray()) { int size = schemaNode.size(); for (int i = 0; i < size; i++) { diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index a8ecb4bdc..f370be93b 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,12 +29,12 @@ public class TypeValidator extends BaseJsonValidator implements JsonValidator { private JsonType schemaType; private UnionTypeValidator unionTypeValidator; - public TypeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.TYPE); + public TypeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.TYPE, validationContext); schemaType = TypeFactory.getSchemaNodeType(schemaNode); if (schemaType == JsonType.UNION) { - unionTypeValidator = new UnionTypeValidator(schemaPath, schemaNode, parentSchema, mapper); + unionTypeValidator = new UnionTypeValidator(schemaPath, schemaNode, parentSchema, validationContext); } parseErrorCode(getValidatorType().getErrorCodeKey()); diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index a355ad3a2..52534f43f 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,8 +31,8 @@ public class UnionTypeValidator extends BaseJsonValidator implements JsonValidat private List schemas; private String error; - public UnionTypeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNION_TYPE); + public UnionTypeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNION_TYPE, validationContext); schemas = new ArrayList(); StringBuilder errorBuilder = new StringBuilder(); @@ -51,9 +50,9 @@ public UnionTypeValidator(String schemaPath, JsonNode schemaNode, JsonSchema par sep = ", "; if (n.isObject()) - schemas.add(new JsonSchema(mapper, ValidatorTypeCode.TYPE.getValue(), n, parentSchema)); + schemas.add(new JsonSchema(validationContext, ValidatorTypeCode.TYPE.getValue(), n, parentSchema)); else - schemas.add(new TypeValidator(schemaPath + "/" + i, n, parentSchema, mapper)); + schemas.add(new TypeValidator(schemaPath + "/" + i, n, parentSchema, validationContext)); i++; } diff --git a/src/main/java/com/networknt/schema/UniqueItemsValidator.java b/src/main/java/com/networknt/schema/UniqueItemsValidator.java index ccac9d3b6..05eabc05e 100644 --- a/src/main/java/com/networknt/schema/UniqueItemsValidator.java +++ b/src/main/java/com/networknt/schema/UniqueItemsValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,8 +29,8 @@ public class UniqueItemsValidator extends BaseJsonValidator implements JsonValid private boolean unique = false; - public UniqueItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ObjectMapper mapper) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNIQUE_ITEMS); + public UniqueItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNIQUE_ITEMS, validationContext); if (schemaNode.isBoolean()) { unique = schemaNode.booleanValue(); } diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java new file mode 100644 index 000000000..3166839c1 --- /dev/null +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -0,0 +1,30 @@ +package com.networknt.schema; + +import java.util.Optional; + +import com.fasterxml.jackson.databind.JsonNode; + +public class ValidationContext { + private final JsonMetaSchema metaSchema; + private final JsonSchemaFactory jsonSchemaFactory; + + public ValidationContext(JsonMetaSchema metaSchema, JsonSchemaFactory jsonSchemaFactory) { + if (metaSchema == null) { + throw new IllegalArgumentException("JsonMetaSchema must not be null"); + } + if (jsonSchemaFactory == null) { + throw new IllegalArgumentException("JsonSchemaFactory must not be null"); + } + this.metaSchema = metaSchema; + this.jsonSchemaFactory = jsonSchemaFactory; + } + + public Optional newValidator(String schemaPath, String keyword /* keyword */, JsonNode schemaNode, + JsonSchema parentSchema) { + return metaSchema.newValidator(this, schemaPath, keyword, schemaNode, parentSchema); + } + + public JsonSchemaFactory getJsonSchemaFactory() { + return jsonSchemaFactory; + } +} diff --git a/src/main/java/com/networknt/schema/ValidationMessage.java b/src/main/java/com/networknt/schema/ValidationMessage.java index b1d4044e9..fe218bed3 100644 --- a/src/main/java/com/networknt/schema/ValidationMessage.java +++ b/src/main/java/com/networknt/schema/ValidationMessage.java @@ -18,12 +18,14 @@ import java.text.MessageFormat; import java.util.Arrays; +import java.util.Map; public class ValidationMessage { private String type; private String code; private String path; private String[] arguments; + private Map details; private String message; ValidationMessage() { @@ -52,6 +54,14 @@ public String[] getArguments() { void setArguments(String[] arguments) { this.arguments = arguments; } + + void setDetails(Map details) { + this.details = details; + } + + public Map getDetails() { + return details; + } public String getMessage() { return message; @@ -76,6 +86,7 @@ public boolean equals(Object o) { if (type != null ? !type.equals(that.type) : that.type != null) return false; if (code != null ? !code.equals(that.code) : that.code != null) return false; if (path != null ? !path.equals(that.path) : that.path != null) return false; + if (details != null ? !details.equals(that.details) : that.details != null) return false; if (!Arrays.equals(arguments, that.arguments)) return false; return !(message != null ? !message.equals(that.message) : that.message != null); @@ -86,6 +97,7 @@ public int hashCode() { int result = type != null ? type.hashCode() : 0; result = 31 * result + (code != null ? code.hashCode() : 0); result = 31 * result + (path != null ? path.hashCode() : 0); + result = 31 * result + (details != null ? details.hashCode() : 0); result = 31 * result + (arguments != null ? Arrays.hashCode(arguments) : 0); result = 31 * result + (message != null ? message.hashCode() : 0); return result; @@ -99,11 +111,26 @@ public void setType(String type) { this.type = type; } + public static ValidationMessage of(String type, ErrorMessageType errorMessageType, String at, String... arguments) { + ValidationMessage.Builder builder = new ValidationMessage.Builder(); + builder.code(errorMessageType.getErrorCode()).path(at).arguments(arguments) + .format(errorMessageType.getMessageFormat()).type(type); + return builder.build(); + } + + public static ValidationMessage of(String type, ErrorMessageType errorMessageType, String at, Map details) { + ValidationMessage.Builder builder = new ValidationMessage.Builder(); + builder.code(errorMessageType.getErrorCode()).path(at).details(details) + .format(errorMessageType.getMessageFormat()).type(type); + return builder.build(); + } + public static class Builder { private String type; private String code; private String path; private String[] arguments; + private Map details; private MessageFormat format; public Builder type(String type) { @@ -125,6 +152,11 @@ public Builder arguments(String... arguments) { this.arguments = arguments; return this; } + + public Builder details(Map details) { + this.details = details; + return this; + } public Builder format(MessageFormat format) { this.format = format; @@ -137,6 +169,7 @@ public ValidationMessage build() { msg.setCode(code); msg.setPath(path); msg.setArguments(arguments); + msg.setDetails(details); if (format != null) { String[] objs = new String[(arguments == null ? 0 : arguments.length) + 1]; diff --git a/src/main/java/com/networknt/schema/ValidatorTypeCode.java b/src/main/java/com/networknt/schema/ValidatorTypeCode.java index 5241af678..d6a1121b3 100644 --- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java +++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java @@ -16,11 +16,14 @@ package com.networknt.schema; +import java.lang.reflect.Constructor; import java.text.MessageFormat; -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; -public enum ValidatorTypeCode { +import com.fasterxml.jackson.databind.JsonNode; + +public enum ValidatorTypeCode implements Keyword, ErrorMessageType { ADDITIONAL_PROPERTIES("additionalProperties", "1001", new MessageFormat( "{0}.{1}: is not defined in the schema and the schema does not allow additional properties")), ALL_OF("allOf", "1002", new MessageFormat("{0}: should be valid to all the schemas {1}")), @@ -29,7 +32,13 @@ public enum ValidatorTypeCode { DEPENDENCIES("dependencies", "1007", new MessageFormat("{0}: has an error with dependencies {1}")), EDITS("edits", "1005", new MessageFormat("{0}: has an error with 'edits'")), ENUM("enum", "1008", new MessageFormat("{0}: does not have a value in the enumeration {1}")), - FORMAT("format", "1009", new MessageFormat("{0}: does not match the {1} pattern {2}")), + FORMAT("format", "1009", new MessageFormat("{0}: does not match the {1} pattern {2}")){ + @Override + public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) + throws Exception { + throw new UnsupportedOperationException("Use FormatKeyword instead"); + } + }, ITEMS("items", "1010", new MessageFormat("{0}[{1}]: no validator found at this index")), MAXIMUM("maximum", "1011", new MessageFormat("{0}: must have a maximum value of {1}")), MAX_ITEMS("maxItems", "1012", new MessageFormat("{0}: there must be a maximum of {1} items in the array")), @@ -47,20 +56,12 @@ public enum ValidatorTypeCode { PATTERN("pattern", "1023", new MessageFormat("{0}: does not match the regex pattern {1}")), PROPERTIES("properties", "1025", new MessageFormat("{0}: has an error with 'properties'")), READ_ONLY("readOnly", "1032", new MessageFormat("{0}: is a readonly field, it cannot be changed")), - REF("ref", "1026", new MessageFormat("{0}: has an error with 'refs'")), + REF("$ref", "1026", new MessageFormat("{0}: has an error with 'refs'")), REQUIRED("required", "1028", new MessageFormat("{0}.{1}: is missing but it is required")), TYPE("type", "1029", new MessageFormat("{0}: {1} found, {2} expected")), UNION_TYPE("unionType", "1030", new MessageFormat("{0}: {1} found, but {2} is required")), UNIQUE_ITEMS("uniqueItems", "1031", new MessageFormat("{0}: the items in the array must be unique")); - private static Map constants = new HashMap(); - - static { - for (ValidatorTypeCode c : values()) { - constants.put(c.value, c); - } - } - private final String value; private final String errorCode; private final MessageFormat messageFormat; @@ -73,15 +74,34 @@ private ValidatorTypeCode(String value, String errorCode, MessageFormat messageF this.errorCodeKey = value + "ErrorCode"; } - public static ValidatorTypeCode fromValue(String value) { - ValidatorTypeCode constant = constants.get(value); - if (constant == null) { - throw new IllegalArgumentException(value); - } else { - return constant; + public static List getNonFormatKeywords() { + final List result = new ArrayList(); + for (ValidatorTypeCode keyword: values()) { + if (!FORMAT.equals(keyword)) { + result.add(keyword); + } } + return result; } + + public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws Exception { + String shortClassName = getValue(); + if (shortClassName.startsWith("$")) { + // remove "$" from class name for $ref schema + shortClassName = shortClassName.substring(1); + } + final String className = Character.toUpperCase(shortClassName.charAt(0)) + shortClassName.substring(1) + + "Validator"; + @SuppressWarnings("unchecked") + final Class clazz = (Class) Class + .forName("com.networknt.schema." + className); + Constructor c = null; + c = clazz.getConstructor( + new Class[] { String.class, JsonNode.class, JsonSchema.class, ValidationContext.class }); + return c.newInstance(schemaPath + "/" + getValue(), schemaNode, parentSchema, validationContext); + } + @Override public String toString() { return this.value; diff --git a/src/main/java/com/networknt/schema/url/StandardURLFetcher.java b/src/main/java/com/networknt/schema/url/StandardURLFetcher.java new file mode 100644 index 000000000..3d99be4b8 --- /dev/null +++ b/src/main/java/com/networknt/schema/url/StandardURLFetcher.java @@ -0,0 +1,16 @@ +package com.networknt.schema.url; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +/** + * Standard URL fetcher that uses {@link URL#openStream()} for fetching. + */ +public class StandardURLFetcher implements URLFetcher { + + @Override + public InputStream fetch(URL url) throws IOException { + return url.openStream(); + } +} diff --git a/src/main/java/com/networknt/schema/url/URLFetcher.java b/src/main/java/com/networknt/schema/url/URLFetcher.java new file mode 100644 index 000000000..7c0308f81 --- /dev/null +++ b/src/main/java/com/networknt/schema/url/URLFetcher.java @@ -0,0 +1,9 @@ +package com.networknt.schema.url; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +public interface URLFetcher { + InputStream fetch(URL url) throws IOException; +} diff --git a/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java b/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java index 9bd0fa6dc..9ee0785e7 100644 --- a/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java +++ b/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java @@ -48,27 +48,28 @@ protected JsonNode getJsonNodeFromUrl(String url) throws Exception { } protected JsonSchema getJsonSchemaFromClasspath(String name) throws Exception { - JsonSchemaFactory factory = new JsonSchemaFactory(); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(); InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream(name); JsonSchema schema = factory.getSchema(is); return schema; } + protected JsonSchema getJsonSchemaFromStringContent(String schemaContent) throws Exception { - JsonSchemaFactory factory = new JsonSchemaFactory(); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(); JsonSchema schema = factory.getSchema(schemaContent); return schema; } protected JsonSchema getJsonSchemaFromUrl(String url) throws Exception { - JsonSchemaFactory factory = new JsonSchemaFactory(); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(); JsonSchema schema = factory.getSchema(new URL(url)); return schema; } protected JsonSchema getJsonSchemaFromJsonNode(JsonNode jsonNode) throws Exception { - JsonSchemaFactory factory = new JsonSchemaFactory(); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(); JsonSchema schema = factory.getSchema(jsonNode); return schema; } diff --git a/src/test/java/com/networknt/schema/JsonSchemaTest.java b/src/test/java/com/networknt/schema/JsonSchemaTest.java index 9c3b71e20..33ba586fd 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaTest.java @@ -20,8 +20,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import io.undertow.Undertow; -import io.undertow.server.HttpHandler; -import io.undertow.server.handlers.resource.FileResource; import io.undertow.server.handlers.resource.FileResourceManager; import org.junit.AfterClass; import org.junit.Assert; @@ -38,6 +36,7 @@ public class JsonSchemaTest { protected ObjectMapper mapper = new ObjectMapper(); + protected JsonSchemaFactory validatorFactory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance()).objectMapper(mapper).build(); protected static Undertow server = null; public JsonSchemaTest() { @@ -75,7 +74,7 @@ private void runTestFile(String testCaseFile) throws Exception { for (int j = 0; j < testCases.size(); j++) { try { JsonNode testCase = testCases.get(j); - JsonSchema schema = new JsonSchema(mapper, testCase.get("schema")); + JsonSchema schema = validatorFactory.getSchema(testCase.get("schema")); ArrayNode testNodes = (ArrayNode) testCase.get("tests"); for (int i = 0; i < testNodes.size(); i++) { JsonNode test = testNodes.get(i); @@ -110,7 +109,8 @@ private void runTestFile(String testCaseFile) throws Exception { public void testLoadingWithId() throws Exception { URL url = new URL("http://localhost:1234/self_ref/selfRef.json"); JsonNode schemaJson = mapper.readTree(url); - JsonSchemaFactory factory = new JsonSchemaFactory(); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(); + @SuppressWarnings("unused") JsonSchema schema = factory.getSchema(schemaJson); } diff --git a/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java b/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java index 43e716fad..4c6d6c7d5 100644 --- a/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java +++ b/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java @@ -20,7 +20,6 @@ import org.junit.Assert; import org.junit.Test; -import java.util.List; import java.util.Set; /** @@ -30,7 +29,7 @@ public class PatternPropertiesValidatorTest extends BaseJsonSchemaValidatorTest @Test(expected=JsonSchemaException.class) public void testInvalidPatternPropertiesValidator() throws Exception { - JsonSchemaFactory factory = new JsonSchemaFactory(); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(); JsonSchema schema = factory.getSchema("{\"patternProperties\":6}"); JsonNode node = getJsonNodeFromStringContent(""); From e4a7af3abb9a471f8c18a90ed6d167a34bc4a722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klas=20Kala=C3=9F?= Date: Wed, 22 Nov 2017 16:24:09 +0100 Subject: [PATCH 2/2] Improved handling for unknown keywords --- .../com/networknt/schema/JsonMetaSchema.java | 18 ++++++++-- .../networknt/schema/JsonSchemaFactory.java | 7 ++-- .../schema/NonValidationKeyword.java | 33 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/networknt/schema/NonValidationKeyword.java diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index 1855cfe14..8fc64aaa8 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -18,12 +18,14 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -36,7 +38,7 @@ public class JsonMetaSchema { private static final Logger logger = LoggerFactory .getLogger(JsonMetaSchema.class); - + private static Map UNKNOWN_KEYWORDS = new ConcurrentHashMap(); private static class DraftV4 { private static String URI = "http://json-schema.org/draft-04/schema#"; @@ -76,6 +78,15 @@ public static JsonMetaSchema getInstance() { .idKeyword(DRAFT_4_ID) .addFormats(BUILTIN_FORMATS) .addKeywords(ValidatorTypeCode.getNonFormatKeywords()) + // keywords that may validly exist, but have no validation aspect to them + .addKeywords(Arrays.asList( + new NonValidationKeyword("$schema"), + new NonValidationKeyword("id"), + new NonValidationKeyword("title"), + new NonValidationKeyword("description"), + new NonValidationKeyword("default"), + new NonValidationKeyword("definitions") + )) .build(); } } @@ -221,7 +232,10 @@ public Optional newValidator(ValidationContext validationContext, try { Keyword kw = keywords.get(keyword); if (kw == null) { - logger.warn("Unknown keyword " + keyword); + if (!UNKNOWN_KEYWORDS.containsKey(keyword)) { + UNKNOWN_KEYWORDS.put(keyword, keyword); + logger.warn("Unknown keyword " + keyword + " - you should define your own Meta Schema. If the keyword is irrelevant for validation, just use a NonValidationKeyword"); + } return Optional.empty(); } return Optional.of(kw.newValidator(schemaPath, schemaNode, parentSchema, validationContext)); diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index c3deef8c6..fa67982a4 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -214,8 +214,11 @@ private boolean idMatchesSourceUrl(JsonMetaSchema metaSchema, JsonNode schema, U if (id == null || id.isEmpty()) { return false; } - logger.info("Matching " + id + " to " + schemaUrl.toString()); - return id.equals(schemaUrl.toString()); + boolean result = id.equals(schemaUrl.toString()); + if (logger.isDebugEnabled()) { + logger.debug("Matching " + id + " to " + schemaUrl.toString() + ": " + result); + } + return result; } diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java new file mode 100644 index 000000000..0a0e06587 --- /dev/null +++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java @@ -0,0 +1,33 @@ +package com.networknt.schema; + +import java.util.Collections; +import java.util.Set; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Used for Keywords that have no validation aspect, but are part of the metaschema. + */ +public class NonValidationKeyword extends AbstractKeyword { + + private static final class Validator extends AbstractJsonValidator { + private Validator(String keyword) { + super(keyword); + } + + @Override + public Set validate(JsonNode node, JsonNode rootNode, String at) { + return Collections.emptySet(); + } + } + + public NonValidationKeyword(String keyword) { + super(keyword); + } + + @Override + public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, + ValidationContext validationContext) throws JsonSchemaException, Exception { + return new Validator(getValue()); + } +}