diff --git a/src/main/java/com/networknt/schema/AbstractJsonValidator.java b/src/main/java/com/networknt/schema/AbstractJsonValidator.java index 8dff5eb04..bf3fc5d53 100644 --- a/src/main/java/com/networknt/schema/AbstractJsonValidator.java +++ b/src/main/java/com/networknt/schema/AbstractJsonValidator.java @@ -28,7 +28,7 @@ public abstract class AbstractJsonValidator implements JsonValidator { private final SchemaLocation schemaLocation; private final JsonNode schemaNode; private final JsonNodePath evaluationPath; - private final Keyword keyword; + private final String keyword; /** * Constructor. @@ -41,7 +41,7 @@ public abstract class AbstractJsonValidator implements JsonValidator { public AbstractJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Keyword keyword, JsonNode schemaNode) { this.schemaLocation = schemaLocation; this.evaluationPath = evaluationPath; - this.keyword = keyword; + this.keyword = keyword.getValue(); this.schemaNode = schemaNode; } @@ -57,7 +57,7 @@ public JsonNodePath getEvaluationPath() { @Override public String getKeyword() { - return keyword.getValue(); + return keyword; } /** diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index 2cca133ef..62262ba6e 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -23,7 +23,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; /** * {@link JsonValidator} for additionalProperties. @@ -34,7 +41,7 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator { private final boolean allowAdditionalProperties; private final JsonSchema additionalPropertiesSchema; private final Set allowedProperties; - private final List patternProperties = new ArrayList<>(); + private final List patternProperties; private Boolean hasUnevaluatedPropertiesValidator; @@ -64,9 +71,12 @@ public AdditionalPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath JsonNode patternPropertiesNode = parentSchema.getSchemaNode().get(PatternPropertiesValidator.PROPERTY); if (patternPropertiesNode != null) { + this.patternProperties = new ArrayList<>(); for (Iterator it = patternPropertiesNode.fieldNames(); it.hasNext(); ) { patternProperties.add(RegularExpression.compile(it.next(), validationContext)); } + } else { + this.patternProperties = Collections.emptyList(); } } @@ -93,8 +103,9 @@ public Set validate(ExecutionContext executionContext, JsonNo Set errors = null; - for (Iterator it = node.fieldNames(); it.hasNext(); ) { - String pname = it.next(); + for (Iterator> it = node.fields(); it.hasNext(); ) { + Entry entry = it.next(); + String pname = entry.getKey(); // skip the context items if (pname.startsWith("#")) { continue; @@ -120,7 +131,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (additionalPropertiesSchema != null) { ValidatorState state = executionContext.getValidatorState(); if (state != null && state.isWalkEnabled()) { - Set results = additionalPropertiesSchema.walk(executionContext, node.get(pname), rootNode, instanceLocation.append(pname), state.isValidationEnabled()); + Set results = additionalPropertiesSchema.walk(executionContext, + entry.getValue(), rootNode, instanceLocation.append(pname), + state.isValidationEnabled()); if (!results.isEmpty()) { if (errors == null) { errors = new LinkedHashSet<>(); @@ -128,7 +141,8 @@ public Set validate(ExecutionContext executionContext, JsonNo errors.addAll(results); } } else { - Set results = additionalPropertiesSchema.validate(executionContext, node.get(pname), rootNode, instanceLocation.append(pname)); + Set results = additionalPropertiesSchema.validate(executionContext, + entry.getValue(), rootNode, instanceLocation.append(pname)); if (!results.isEmpty()) { if (errors == null) { errors = new LinkedHashSet<>(); diff --git a/src/main/java/com/networknt/schema/AnnotationKeyword.java b/src/main/java/com/networknt/schema/AnnotationKeyword.java new file mode 100644 index 000000000..e0d7d5c82 --- /dev/null +++ b/src/main/java/com/networknt/schema/AnnotationKeyword.java @@ -0,0 +1,68 @@ +/* + * 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 com.fasterxml.jackson.databind.JsonNode; + +import java.util.Collections; +import java.util.Set; + +/** + * Used for Keywords that have no validation aspect, but are part of the metaschema, where annotations may need to be collected. + */ +public class AnnotationKeyword extends AbstractKeyword { + + private static final class Validator extends AbstractJsonValidator { + public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext, Keyword keyword) { + super(schemaLocation, evaluationPath, keyword, schemaNode); + } + + @Override + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + if (collectAnnotations(executionContext)) { + Object value = getAnnotationValue(getSchemaNode()); + if (value != null) { + putAnnotation(executionContext, + annotation -> annotation.instanceLocation(instanceLocation).value(value)); + } + } + return Collections.emptySet(); + } + + protected Object getAnnotationValue(JsonNode schemaNode) { + if (schemaNode.isTextual()) { + return schemaNode.textValue(); + } else if (schemaNode.isNumber()) { + return schemaNode.numberValue(); + } else if (schemaNode.isObject()) { + return schemaNode; + } + return null; + } + } + + public AnnotationKeyword(String keyword) { + super(keyword); + } + + @Override + public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { + return new Validator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, this); + } +} diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 0f52a9040..75c2a08b7 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -290,12 +290,40 @@ protected void preloadJsonSchemas(final Collection schemas) { } } + public static class JsonNodePathLegacy { + private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.LEGACY); + public static JsonNodePath getInstance() { + return INSTANCE; + } + } + + public static class JsonNodePathJsonPointer { + private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.JSON_POINTER); + public static JsonNodePath getInstance() { + return INSTANCE; + } + } + + public static class JsonNodePathJsonPath { + private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.JSON_PATH); + public static JsonNodePath getInstance() { + return INSTANCE; + } + } + /** * Get the root path. * * @return The path. */ protected JsonNodePath atRoot() { + if (this.validationContext.getConfig().getPathType().equals(PathType.JSON_POINTER)) { + return JsonNodePathJsonPointer.getInstance(); + } else if (this.validationContext.getConfig().getPathType().equals(PathType.LEGACY)) { + return JsonNodePathLegacy.getInstance(); + } else if (this.validationContext.getConfig().getPathType().equals(PathType.JSON_PATH)) { + return JsonNodePathJsonPath.getInstance(); + } return new JsonNodePath(this.validationContext.getConfig().getPathType()); } diff --git a/src/main/java/com/networknt/schema/ExecutionContext.java b/src/main/java/com/networknt/schema/ExecutionContext.java index fc32d2fbb..8ca79cd3c 100644 --- a/src/main/java/com/networknt/schema/ExecutionContext.java +++ b/src/main/java/com/networknt/schema/ExecutionContext.java @@ -26,11 +26,11 @@ */ public class ExecutionContext { private ExecutionConfig executionConfig; - private CollectorContext collectorContext; + private CollectorContext collectorContext = null; private ValidatorState validatorState = null; - private Stack discriminatorContexts = new Stack<>(); - private JsonNodeAnnotations annotations = new JsonNodeAnnotations(); - private JsonNodeResults results = new JsonNodeResults(); + private Stack discriminatorContexts = null; + private JsonNodeAnnotations annotations = null; + private JsonNodeResults results = null; /** * This is used during the execution to determine if the validator should fail fast. @@ -43,7 +43,7 @@ public class ExecutionContext { * Creates an execution context. */ public ExecutionContext() { - this(new CollectorContext()); + this(new ExecutionConfig(), null); } /** @@ -61,7 +61,7 @@ public ExecutionContext(CollectorContext collectorContext) { * @param executionConfig the execution configuration */ public ExecutionContext(ExecutionConfig executionConfig) { - this(executionConfig, new CollectorContext()); + this(executionConfig, null); } /** @@ -81,7 +81,10 @@ public ExecutionContext(ExecutionConfig executionConfig, CollectorContext collec * @return the collector context */ public CollectorContext getCollectorContext() { - return collectorContext; + if (this.collectorContext == null) { + this.collectorContext = new CollectorContext(); + } + return this.collectorContext; } /** @@ -112,10 +115,16 @@ public void setExecutionConfig(ExecutionConfig executionConfig) { } public JsonNodeAnnotations getAnnotations() { + if (this.annotations == null) { + this.annotations = new JsonNodeAnnotations(); + } return annotations; } public JsonNodeResults getResults() { + if (this.results == null) { + this.results = new JsonNodeResults(); + } return results; } @@ -163,6 +172,10 @@ public void setValidatorState(ValidatorState validatorState) { } public DiscriminatorContext getCurrentDiscriminatorContext() { + if (this.discriminatorContexts == null) { + return null; + } + if (!this.discriminatorContexts.empty()) { return this.discriminatorContexts.peek(); } @@ -170,6 +183,9 @@ public DiscriminatorContext getCurrentDiscriminatorContext() { } public void enterDiscriminatorContext(final DiscriminatorContext ctx, @SuppressWarnings("unused") JsonNodePath instanceLocation) { + if (this.discriminatorContexts == null) { + this.discriminatorContexts = new Stack<>(); + } this.discriminatorContexts.push(ctx); } diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 174960039..6c5f1d3c1 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -27,7 +27,15 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; /** @@ -451,10 +459,11 @@ private List read(JsonNode schemaNode) { } else { JsonValidator refValidator = null; - Iterator pnames = schemaNode.fieldNames(); - while (pnames.hasNext()) { - String pname = pnames.next(); - JsonNode nodeToUse = schemaNode.get(pname); + Iterator> iterator = schemaNode.fields(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + String pname = entry.getKey(); + JsonNode nodeToUse = entry.getValue(); JsonNodePath path = getEvaluationPath().append(pname); SchemaLocation schemaPath = getSchemaLocation().append(pname); @@ -536,7 +545,7 @@ private long activeDialect() { @Override public Set validate(ExecutionContext executionContext, JsonNode jsonNode, JsonNode rootNode, JsonNodePath instanceLocation) { - if (validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { + if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator"); if (null != discriminator && null != executionContext.getCurrentDiscriminatorContext()) { executionContext.getCurrentDiscriminatorContext().registerDiscriminator(schemaLocation, @@ -544,7 +553,6 @@ public Set validate(ExecutionContext executionContext, JsonNo } } - SchemaValidatorsConfig config = this.validationContext.getConfig(); SetView errors = null; // Set the walkEnabled and isValidationEnabled flag in internal validator state. setValidatorState(executionContext, false, true); @@ -566,7 +574,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } } - if (config.isOpenAPI3StyleDiscriminators()) { + if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { ObjectNode discriminator = (ObjectNode) this.schemaNode.get("discriminator"); if (null != discriminator) { final DiscriminatorContext discriminatorContext = executionContext @@ -1137,15 +1145,13 @@ public boolean isRecursiveAnchor() { */ public ExecutionContext createExecutionContext() { SchemaValidatorsConfig config = validationContext.getConfig(); - CollectorContext collectorContext = new CollectorContext(); - // Copy execution config defaults from validation config ExecutionConfig executionConfig = new ExecutionConfig(); executionConfig.setLocale(config.getLocale()); executionConfig.setFormatAssertionsEnabled(config.getFormatAssertionsEnabled()); executionConfig.setFailFast(config.isFailFast()); - ExecutionContext executionContext = new ExecutionContext(executionConfig, collectorContext); + ExecutionContext executionContext = new ExecutionContext(executionConfig); if(config.getExecutionContextCustomizer() != null) { config.getExecutionContextCustomizer().customize(executionContext, validationContext); } diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java index df5d9d1e9..573ebea01 100644 --- a/src/main/java/com/networknt/schema/NonValidationKeyword.java +++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java @@ -27,15 +27,11 @@ * Used for Keywords that have no validation aspect, but are part of the metaschema. */ public class NonValidationKeyword extends AbstractKeyword { - private final boolean collectAnnotations; private static final class Validator extends AbstractJsonValidator { - private final boolean collectAnnotations; - public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, - JsonSchema parentSchema, ValidationContext validationContext, Keyword keyword, boolean collectAnnotations) { + JsonSchema parentSchema, ValidationContext validationContext, Keyword keyword) { super(schemaLocation, evaluationPath, keyword, schemaNode); - this.collectAnnotations = collectAnnotations; String id = validationContext.resolveSchemaId(schemaNode); String anchor = validationContext.getMetaSchema().readAnchor(schemaNode); String dynamicAnchor = validationContext.getMetaSchema().readDynamicAnchor(schemaNode); @@ -56,40 +52,17 @@ public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Jso @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - if (collectAnnotations && collectAnnotations(executionContext)) { - Object value = getAnnotationValue(getSchemaNode()); - if (value != null) { - putAnnotation(executionContext, - annotation -> annotation.instanceLocation(instanceLocation).value(value)); - } - } return Collections.emptySet(); } - - protected Object getAnnotationValue(JsonNode schemaNode) { - if (schemaNode.isTextual()) { - return schemaNode.textValue(); - } else if (schemaNode.isNumber()) { - return schemaNode.numberValue(); - } else if (schemaNode.isObject()) { - return schemaNode; - } - return null; - } } public NonValidationKeyword(String keyword) { - this(keyword, true); - } - - public NonValidationKeyword(String keyword, boolean collectAnnotations) { super(keyword); - this.collectAnnotations = collectAnnotations; } @Override public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { - return new Validator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, this, collectAnnotations); + return new Validator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, this); } } diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index 2eb23b271..45e055d85 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -25,7 +25,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; /** * {@link JsonValidator} for properties. @@ -39,10 +45,11 @@ public class PropertiesValidator extends BaseJsonValidator { public PropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PROPERTIES, validationContext); - for (Iterator it = schemaNode.fieldNames(); it.hasNext(); ) { - String pname = it.next(); + for (Iterator> it = schemaNode.fields(); it.hasNext();) { + Entry entry = it.next(); + String pname = entry.getKey(); this.schemas.put(pname, validationContext.newSchema(schemaLocation.append(pname), - evaluationPath.append(pname), schemaNode.get(pname), parentSchema)); + evaluationPath.append(pname), entry.getValue(), parentSchema)); } } @@ -58,8 +65,7 @@ public Set validate(ExecutionContext executionContext, JsonNo Set requiredErrors = null; Set matchedInstancePropertyNames = null; boolean collectAnnotations = collectAnnotations() || collectAnnotations(executionContext); - for (Map.Entry entry : this.schemas.entrySet()) { - JsonSchema propertySchema = entry.getValue(); + for (Entry entry : this.schemas.entrySet()) { JsonNode propertyNode = node.get(entry.getKey()); if (propertyNode != null) { JsonNodePath path = instanceLocation.append(entry.getKey()); @@ -72,15 +78,15 @@ public Set validate(ExecutionContext executionContext, JsonNo // check whether this is a complex validator. save the state boolean isComplex = state.isComplexValidator(); // if this is a complex validator, the node has matched, and all it's child elements, if available, are to be validated - if (state.isComplexValidator()) { + if (isComplex) { state.setMatchedNode(true); + // reset the complex validator for child element validation, and reset it after the return from the recursive call + state.setComplexValidator(false); } - // reset the complex validator for child element validation, and reset it after the return from the recursive call - state.setComplexValidator(false); - if (!state.isWalkEnabled()) { //validate the child element(s) - Set result = propertySchema.validate(executionContext, propertyNode, rootNode, path); + Set result = entry.getValue().validate(executionContext, propertyNode, rootNode, + path); if (!result.isEmpty()) { if (errors == null) { errors = new SetView<>(); @@ -96,9 +102,9 @@ public Set validate(ExecutionContext executionContext, JsonNo } // reset the complex flag to the original value before the recursive call - state.setComplexValidator(isComplex); - // if this was a complex validator, the node has matched and has been validated - if (state.isComplexValidator()) { + if (isComplex) { + state.setComplexValidator(isComplex); + // if this was a complex validator, the node has matched and has been validated state.setMatchedNode(true); } } else { diff --git a/src/main/java/com/networknt/schema/ValidationMessage.java b/src/main/java/com/networknt/schema/ValidationMessage.java index 0bb24160d..41850940d 100644 --- a/src/main/java/com/networknt/schema/ValidationMessage.java +++ b/src/main/java/com/networknt/schema/ValidationMessage.java @@ -357,16 +357,18 @@ public ValidationMessage build() { if (StringUtils.isNotBlank(this.message)) { messageKey = this.message; if (this.message.contains("{")) { - Object[] objs = getMessageArguments(); - MessageFormat format = new MessageFormat(this.message); - messageSupplier = new CachingSupplier<>(() -> format.format(objs)); + messageSupplier = new CachingSupplier<>(() -> { + MessageFormat format = new MessageFormat(this.message); + return format.format(getMessageArguments()); + }); } else { messageSupplier = message::toString; } } else if (messageSupplier == null) { - Object[] objs = getMessageArguments(); - MessageFormatter formatter = this.messageFormatter != null ? this.messageFormatter : format::format; - messageSupplier = new CachingSupplier<>(() -> formatter.format(objs)); + messageSupplier = new CachingSupplier<>(() -> { + MessageFormatter formatter = this.messageFormatter != null ? this.messageFormatter : format::format; + return formatter.format(getMessageArguments()); + }); } return new ValidationMessage(type, code, evaluationPath, schemaLocation, instanceLocation, property, arguments, details, messageKey, messageSupplier, this.instanceNode, this.schemaNode); diff --git a/src/main/java/com/networknt/schema/Version201909.java b/src/main/java/com/networknt/schema/Version201909.java index 0972bdcb0..95c1f8185 100644 --- a/src/main/java/com/networknt/schema/Version201909.java +++ b/src/main/java/com/networknt/schema/Version201909.java @@ -33,23 +33,23 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V201909)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$recursiveAnchor", false), - new NonValidationKeyword("$schema", false), - new NonValidationKeyword("$vocabulary", false), - new NonValidationKeyword("$id", false), - new NonValidationKeyword("title"), - new NonValidationKeyword("description"), - new NonValidationKeyword("default"), - new NonValidationKeyword("definitions", false), + new NonValidationKeyword("$recursiveAnchor"), + new NonValidationKeyword("$schema"), + new NonValidationKeyword("$vocabulary"), + new NonValidationKeyword("$id"), + new AnnotationKeyword("title"), + new AnnotationKeyword("description"), + new AnnotationKeyword("default"), + new NonValidationKeyword("definitions"), new NonValidationKeyword("$comment"), - new NonValidationKeyword("$defs", false), // newly added in 2019-09 release. - new NonValidationKeyword("$anchor", false), + new NonValidationKeyword("$defs"), // newly added in 2019-09 release. + new NonValidationKeyword("$anchor"), new NonValidationKeyword("additionalItems"), - new NonValidationKeyword("deprecated"), - new NonValidationKeyword("contentMediaType"), - new NonValidationKeyword("contentEncoding"), - new NonValidationKeyword("contentSchema"), - new NonValidationKeyword("examples"), + new AnnotationKeyword("deprecated"), + new AnnotationKeyword("contentMediaType"), + new AnnotationKeyword("contentEncoding"), + new AnnotationKeyword("contentSchema"), + new AnnotationKeyword("examples"), new NonValidationKeyword("then"), new NonValidationKeyword("else") )) diff --git a/src/main/java/com/networknt/schema/Version202012.java b/src/main/java/com/networknt/schema/Version202012.java index 680cc3041..f7ed8c389 100644 --- a/src/main/java/com/networknt/schema/Version202012.java +++ b/src/main/java/com/networknt/schema/Version202012.java @@ -35,22 +35,22 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V202012)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema", false), - new NonValidationKeyword("$id", false), - new NonValidationKeyword("title"), - new NonValidationKeyword("description"), - new NonValidationKeyword("default"), - new NonValidationKeyword("definitions", false), + new NonValidationKeyword("$schema"), + new NonValidationKeyword("$id"), + new AnnotationKeyword("title"), + new AnnotationKeyword("description"), + new AnnotationKeyword("default"), + new NonValidationKeyword("definitions"), new NonValidationKeyword("$comment"), - new NonValidationKeyword("$defs", false), - new NonValidationKeyword("$anchor", false), - new NonValidationKeyword("$dynamicAnchor", false), - new NonValidationKeyword("$vocabulary", false), - new NonValidationKeyword("deprecated"), - new NonValidationKeyword("contentMediaType"), - new NonValidationKeyword("contentEncoding"), - new NonValidationKeyword("contentSchema"), - new NonValidationKeyword("examples"), + new NonValidationKeyword("$defs"), + new NonValidationKeyword("$anchor"), + new NonValidationKeyword("$dynamicAnchor"), + new NonValidationKeyword("$vocabulary"), + new AnnotationKeyword("deprecated"), + new AnnotationKeyword("contentMediaType"), + new AnnotationKeyword("contentEncoding"), + new AnnotationKeyword("contentSchema"), + new AnnotationKeyword("examples"), new NonValidationKeyword("then"), new NonValidationKeyword("else") )) diff --git a/src/main/java/com/networknt/schema/Version4.java b/src/main/java/com/networknt/schema/Version4.java index b9f886599..624832f83 100644 --- a/src/main/java/com/networknt/schema/Version4.java +++ b/src/main/java/com/networknt/schema/Version4.java @@ -19,14 +19,14 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V4)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema", false), - new NonValidationKeyword("id", false), - new NonValidationKeyword("title"), - new NonValidationKeyword("description"), - new NonValidationKeyword("default"), - new NonValidationKeyword("definitions", false), + new NonValidationKeyword("$schema"), + new NonValidationKeyword("id"), + new AnnotationKeyword("title"), + new AnnotationKeyword("description"), + new AnnotationKeyword("default"), + new NonValidationKeyword("definitions"), new NonValidationKeyword("additionalItems"), - new NonValidationKeyword("exampleSetFlag") + new AnnotationKeyword("exampleSetFlag") )) .build(); } diff --git a/src/main/java/com/networknt/schema/Version6.java b/src/main/java/com/networknt/schema/Version6.java index 1f92e37eb..6695d52ce 100644 --- a/src/main/java/com/networknt/schema/Version6.java +++ b/src/main/java/com/networknt/schema/Version6.java @@ -20,13 +20,14 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V6)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema", false), - new NonValidationKeyword("$id", false), - new NonValidationKeyword("title"), - new NonValidationKeyword("description"), - new NonValidationKeyword("default"), + new NonValidationKeyword("$schema"), + new NonValidationKeyword("$id"), + new AnnotationKeyword("title"), + new AnnotationKeyword("description"), + new AnnotationKeyword("default"), new NonValidationKeyword("additionalItems"), - new NonValidationKeyword("definitions", false) + new NonValidationKeyword("definitions"), + new AnnotationKeyword("examples") )) .build(); } diff --git a/src/main/java/com/networknt/schema/Version7.java b/src/main/java/com/networknt/schema/Version7.java index 2558b343e..55261e5e8 100644 --- a/src/main/java/com/networknt/schema/Version7.java +++ b/src/main/java/com/networknt/schema/Version7.java @@ -19,18 +19,17 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V7)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema", false), - new NonValidationKeyword("$id", false), - new NonValidationKeyword("title"), - new NonValidationKeyword("description"), - new NonValidationKeyword("default"), - new NonValidationKeyword("definitions", false), + new NonValidationKeyword("$schema"), + new NonValidationKeyword("$id"), + new AnnotationKeyword("title"), + new AnnotationKeyword("description"), + new AnnotationKeyword("default"), + new NonValidationKeyword("definitions"), new NonValidationKeyword("$comment"), - new NonValidationKeyword("examples"), + new AnnotationKeyword("examples"), new NonValidationKeyword("then"), new NonValidationKeyword("else"), - new NonValidationKeyword("additionalItems"), - new NonValidationKeyword("message", false) + new NonValidationKeyword("additionalItems") )) .build(); } diff --git a/src/main/java/com/networknt/schema/resource/DefaultSchemaLoader.java b/src/main/java/com/networknt/schema/resource/DefaultSchemaLoader.java index f84251ec5..a090719b8 100644 --- a/src/main/java/com/networknt/schema/resource/DefaultSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/DefaultSchemaLoader.java @@ -25,6 +25,7 @@ */ public class DefaultSchemaLoader implements SchemaLoader { private static final List DEFAULT; + private static final MetaSchemaMapper META_SCHEMA_MAPPER = new MetaSchemaMapper(); static { List result = new ArrayList<>(); @@ -50,6 +51,10 @@ public InputStreamSource getSchema(AbsoluteIri absoluteIri) { mappedResult = mapped; } } + AbsoluteIri mapped = META_SCHEMA_MAPPER.map(absoluteIri); + if (mapped != null) { + mappedResult = mapped; + } for (SchemaLoader loader : schemaLoaders) { InputStreamSource result = loader.getSchema(mappedResult); if (result != null) { diff --git a/src/main/java/com/networknt/schema/resource/MetaSchemaMapper.java b/src/main/java/com/networknt/schema/resource/MetaSchemaMapper.java new file mode 100644 index 000000000..7881eced6 --- /dev/null +++ b/src/main/java/com/networknt/schema/resource/MetaSchemaMapper.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * 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.resource; + +import com.networknt.schema.AbsoluteIri; + +/** + * Maps the JSON Schema meta schema to the class path location. + */ +public class MetaSchemaMapper implements SchemaMapper { + private static final char ANCHOR = '#'; + private static final String CLASSPATH_PREFIX = "classpath:"; + private static final String HTTP_JSON_SCHEMA_ORG_PREFIX = "http://json-schema.org/"; + private static final String HTTPS_JSON_SCHEMA_ORG_PREFIX = "https://json-schema.org/"; + + @Override + public AbsoluteIri map(AbsoluteIri absoluteIRI) { + String absoluteIRIString = absoluteIRI != null ? absoluteIRI.toString() : null; + if (absoluteIRIString != null) { + if (absoluteIRIString.startsWith(HTTPS_JSON_SCHEMA_ORG_PREFIX)) { + return AbsoluteIri.of(CLASSPATH_PREFIX + absoluteIRIString.substring(24)); + } else if (absoluteIRIString.startsWith(HTTP_JSON_SCHEMA_ORG_PREFIX)) { + int endIndex = absoluteIRIString.length(); + if (absoluteIRIString.charAt(endIndex - 1) == ANCHOR) { + endIndex = endIndex - 1; + } + return AbsoluteIri.of(CLASSPATH_PREFIX + absoluteIRIString.substring(23, endIndex)); + } + } + return null; + } +} diff --git a/src/main/resources/META-INF/native-image/com.networknt/json-schema-validator/native-image.properties b/src/main/resources/META-INF/native-image/com.networknt/json-schema-validator/native-image.properties new file mode 100644 index 000000000..1d69fb5ea --- /dev/null +++ b/src/main/resources/META-INF/native-image/com.networknt/json-schema-validator/native-image.properties @@ -0,0 +1,2 @@ +Args = -H:ReflectionConfigurationResources=${.}/reflect-config.json \ + -H:ResourceConfigurationResources=${.}/resource-config.json diff --git a/src/main/resources/META-INF/native-image/com.networknt/json-schema-validator/reflect-config.json b/src/main/resources/META-INF/native-image/com.networknt/json-schema-validator/reflect-config.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/src/main/resources/META-INF/native-image/com.networknt/json-schema-validator/reflect-config.json @@ -0,0 +1,2 @@ +[ +] diff --git a/src/main/resources/META-INF/native-image/com.networknt/json-schema-validator/resource-config.json b/src/main/resources/META-INF/native-image/com.networknt/json-schema-validator/resource-config.json new file mode 100644 index 000000000..9e167b124 --- /dev/null +++ b/src/main/resources/META-INF/native-image/com.networknt/json-schema-validator/resource-config.json @@ -0,0 +1,24 @@ +{ + "resources": { + "includes": [ + { + "pattern": "draft/.*" + }, + { + "pattern": "draft-04/.*" + }, + { + "pattern": "draft-06/.*" + }, + { + "pattern": "draft-07/.*" + }, + { + "pattern": "ucd/.*" + }, + { + "pattern": "jsv-messages.*properties" + } + ] + } +} diff --git a/src/main/resources/draftv4.schema.json b/src/main/resources/draftv4.schema.json deleted file mode 100644 index 6d926f6fc..000000000 --- a/src/main/resources/draftv4.schema.json +++ /dev/null @@ -1,221 +0,0 @@ -{ - "id": "http://json-schema.org/draft-04/schema#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Core schema meta-schema", - "definitions": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#" - } - }, - "positiveInteger": { - "type": "integer", - "minimum": 0 - }, - "positiveIntegerDefault0": { - "allOf": [ - { - "$ref": "#/definitions/positiveInteger" - }, - { - "default": 0 - } - ] - }, - "simpleTypes": { - "enum": [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string" - ] - }, - "stringArray": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1, - "uniqueItems": true - } - }, - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uri" - }, - "$schema": { - "type": "string", - "format": "uri" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": {}, - "multipleOf": { - "type": "number", - "minimum": 0, - "exclusiveMinimum": true - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "boolean", - "default": false - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "boolean", - "default": false - }, - "maxLength": { - "$ref": "#/definitions/positiveInteger" - }, - "minLength": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "pattern": { - "type": "string", - "format": "regex" - }, - "additionalItems": { - "anyOf": [ - { - "type": "boolean" - }, - { - "$ref": "#" - } - ], - "default": {} - }, - "items": { - "anyOf": [ - { - "$ref": "#" - }, - { - "$ref": "#/definitions/schemaArray" - } - ], - "default": {} - }, - "maxItems": { - "$ref": "#/definitions/positiveInteger" - }, - "minItems": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "maxProperties": { - "$ref": "#/definitions/positiveInteger" - }, - "minProperties": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "required": { - "$ref": "#/definitions/stringArray" - }, - "additionalProperties": { - "anyOf": [ - { - "type": "boolean" - }, - { - "$ref": "#" - } - ], - "default": {} - }, - "definitions": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "$ref": "#" - }, - { - "$ref": "#/definitions/stringArray" - } - ] - } - }, - "enum": { - "type": "array", - "minItems": 1, - "uniqueItems": true - }, - "type": { - "anyOf": [ - { - "$ref": "#/definitions/simpleTypes" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/simpleTypes" - }, - "minItems": 1, - "uniqueItems": true - } - ] - }, - "allOf": { - "$ref": "#/definitions/schemaArray" - }, - "anyOf": { - "$ref": "#/definitions/schemaArray" - }, - "oneOf": { - "$ref": "#/definitions/schemaArray" - }, - "not": { - "$ref": "#" - } - }, - "dependencies": { - "exclusiveMaximum": [ - "maximum" - ], - "exclusiveMinimum": [ - "minimum" - ] - }, - "default": {} -} diff --git a/src/test/java/com/networknt/schema/resource/MetaSchemaMapperTest.java b/src/test/java/com/networknt/schema/resource/MetaSchemaMapperTest.java new file mode 100644 index 000000000..dd8980455 --- /dev/null +++ b/src/test/java/com/networknt/schema/resource/MetaSchemaMapperTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * 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.resource; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; +import java.io.InputStream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import com.networknt.schema.AbsoluteIri; +import com.networknt.schema.SchemaId; + +/** + * MetaSchemaMapperTest. + */ +class MetaSchemaMapperTest { + + enum MapInput { + V4(SchemaId.V4), + V6(SchemaId.V6), + V7(SchemaId.V7), + V201909(SchemaId.V201909), + V202012(SchemaId.V202012); + + String iri; + + MapInput(String iri) { + this.iri = iri; + } + } + + @ParameterizedTest + @EnumSource(MapInput.class) + void map(MapInput input) throws IOException { + MetaSchemaMapper mapper = new MetaSchemaMapper(); + AbsoluteIri result = mapper.map(AbsoluteIri.of(input.iri)); + ClasspathSchemaLoader loader = new ClasspathSchemaLoader(); + InputStreamSource source = loader.getSchema(result); + assertNotNull(source); + try (InputStream inputStream = source.getInputStream()) { + inputStream.read(); + } + } + +}