diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index 18e4d8b50..1c7171981 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.networknt.schema.CollectorContext.Scope; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,21 +49,11 @@ public Set validate(JsonNode node, JsonNode rootNode, String Set childSchemaErrors = new LinkedHashSet<>(); - Collection newEvaluatedItems = Collections.emptyList(); - Collection newEvaluatedProperties = Collections.emptyList(); - for (JsonSchema schema : this.schemas) { - // As AllOf might contain multiple schemas take a backup of evaluated stuff. - Collection backupEvaluatedItems = collectorContext.getEvaluatedItems(); - Collection backupEvaluatedProperties = collectorContext.getEvaluatedProperties(); - Set localErrors = new HashSet<>(); + Scope parentScope = collectorContext.enterDynamicScope(); try { - // Make the evaluated lists empty. - collectorContext.resetEvaluatedItems(); - collectorContext.resetEvaluatedProperties(); - if (!state.isWalkEnabled()) { localErrors = schema.validate(node, rootNode, at); } else { @@ -70,12 +62,6 @@ public Set validate(JsonNode node, JsonNode rootNode, String childSchemaErrors.addAll(localErrors); - // Keep Collecting total evaluated properties. - if (localErrors.isEmpty()) { - newEvaluatedItems = collectorContext.getEvaluatedItems(); - newEvaluatedProperties = collectorContext.getEvaluatedProperties(); - } - if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { final Iterator arrayElements = this.schemaNode.elements(); while (arrayElements.hasNext()) { @@ -108,14 +94,10 @@ public Set validate(JsonNode node, JsonNode rootNode, String } } } finally { - collectorContext.setEvaluatedItems(backupEvaluatedItems); - collectorContext.setEvaluatedProperties(backupEvaluatedProperties); + Scope scope = collectorContext.exitDynamicScope(); if (localErrors.isEmpty()) { - collectorContext.getEvaluatedItems().addAll(newEvaluatedItems); - collectorContext.getEvaluatedProperties().addAll(newEvaluatedProperties); + parentScope.mergeWith(scope); } - newEvaluatedItems = Collections.emptyList(); - newEvaluatedProperties = Collections.emptyList(); } } diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index 869b1c210..f92e984cc 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -17,6 +17,8 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.CollectorContext.Scope; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,64 +63,64 @@ public Set validate(JsonNode node, JsonNode rootNode, String Set allErrors = new LinkedHashSet<>(); - // As anyOf might contain multiple schemas take a backup of evaluated stuff. - Collection backupEvaluatedItems = collectorContext.getEvaluatedItems(); - Collection backupEvaluatedProperties = collectorContext.getEvaluatedProperties(); - - // Make the evaluated lists empty. - collectorContext.resetEvaluatedItems(); - collectorContext.resetEvaluatedProperties(); - + Scope grandParentScope = collectorContext.enterDynamicScope(); try { int numberOfValidSubSchemas = 0; - for (int i = 0; i < this.schemas.size(); ++i) { - JsonSchema schema = this.schemas.get(i); - state.setMatchedNode(initialHasMatchedNode); - Set errors; - - if (schema.hasTypeValidator()) { - TypeValidator typeValidator = schema.getTypeValidator(); - //If schema has type validator and node type doesn't match with schemaType then ignore it - //For union type, it is a must to call TypeValidator - if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) { - allErrors.add(buildValidationMessage(at, typeValidator.getSchemaType().toString())); - continue; + for (JsonSchema schema: this.schemas) { + Set errors = Collections.emptySet(); + Scope parentScope = collectorContext.enterDynamicScope(); + try { + state.setMatchedNode(initialHasMatchedNode); + + if (schema.hasTypeValidator()) { + TypeValidator typeValidator = schema.getTypeValidator(); + //If schema has type validator and node type doesn't match with schemaType then ignore it + //For union type, it is a must to call TypeValidator + if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) { + allErrors.add(buildValidationMessage(at, typeValidator.getSchemaType().toString())); + continue; + } } - } - if (!state.isWalkEnabled()) { - errors = schema.validate(node, rootNode, at); - } else { - errors = schema.walk(node, rootNode, at, true); - } - - // check if any validation errors have occurred - if (errors.isEmpty()) { - // check whether there are no errors HOWEVER we have validated the exact validator - if (!state.hasMatchedNode()) { - continue; + if (!state.isWalkEnabled()) { + errors = schema.validate(node, rootNode, at); + } else { + errors = schema.walk(node, rootNode, at, true); } - // we found a valid subschema, so increase counter - numberOfValidSubSchemas++; - } - if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators())) { - // Clear all errors. - allErrors.clear(); - // return empty errors. - return errors; - } else if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { - if (this.discriminatorContext.isDiscriminatorMatchFound()) { - if (!errors.isEmpty()) { - errors.add(buildValidationMessage(at, DISCRIMINATOR_REMARK)); - allErrors.addAll(errors); - } else { - // Clear all errors. - allErrors.clear(); + // check if any validation errors have occurred + if (errors.isEmpty()) { + // check whether there are no errors HOWEVER we have validated the exact validator + if (!state.hasMatchedNode()) { + continue; } + // we found a valid subschema, so increase counter + numberOfValidSubSchemas++; + } + + if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators())) { + // Clear all errors. + allErrors.clear(); + // return empty errors. return errors; + } else if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { + if (this.discriminatorContext.isDiscriminatorMatchFound()) { + if (!errors.isEmpty()) { + allErrors.addAll(errors); + allErrors.add(buildValidationMessage(at, DISCRIMINATOR_REMARK)); + } else { + // Clear all errors. + allErrors.clear(); + } + return errors; + } + } + allErrors.addAll(errors); + } finally { + Scope scope = collectorContext.exitDynamicScope(); + if (errors.isEmpty()) { + parentScope.mergeWith(scope); } } - allErrors.addAll(errors); } // determine only those errors which are NOT of type "required" property missing @@ -138,14 +140,12 @@ public Set validate(JsonNode node, JsonNode rootNode, String if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { this.validationContext.leaveDiscriminatorContextImmediately(at); } + + Scope parentScope = collectorContext.exitDynamicScope(); if (allErrors.isEmpty()) { state.setMatchedNode(true); - } else { - collectorContext.getEvaluatedItems().clear(); - collectorContext.getEvaluatedProperties().clear(); + grandParentScope.mergeWith(parentScope); } - collectorContext.getEvaluatedItems().addAll(backupEvaluatedItems); - collectorContext.getEvaluatedProperties().addAll(backupEvaluatedProperties); } return Collections.unmodifiableSet(allErrors); } diff --git a/src/main/java/com/networknt/schema/CollectorContext.java b/src/main/java/com/networknt/schema/CollectorContext.java index 74c44082e..75f627f76 100644 --- a/src/main/java/com/networknt/schema/CollectorContext.java +++ b/src/main/java/com/networknt/schema/CollectorContext.java @@ -15,9 +15,14 @@ */ package com.networknt.schema; +import java.util.AbstractCollection; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Deque; import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -47,62 +52,62 @@ public static CollectorContext getInstance() { */ private Map collectorLoadMap = new HashMap<>(); - /** - * Used to track which array items have been evaluated. - */ - private Collection evaluatedItems = new ArrayList<>(); + private final Deque dynamicScopes = new LinkedList<>(); + private final boolean disableUnevaluatedItems; + private final boolean disableUnevaluatedProperties; - /** - * Used to track which properties have been evaluated. - */ - private Collection evaluatedProperties = new ArrayList<>(); + public CollectorContext() { + this(false, false); + } - /** - * Identifies which array items have been evaluated. - * - * @return the set of evaluated items (never null) - */ - public Collection getEvaluatedItems() { - return this.evaluatedItems; + public CollectorContext(boolean disableUnevaluatedItems, boolean disableUnevaluatedProperties) { + this.disableUnevaluatedItems = disableUnevaluatedItems; + this.disableUnevaluatedProperties = disableUnevaluatedProperties; + this.dynamicScopes.push(newScope()); } /** - * Set the array items that have been evaluated. - * @param paths the set of evaluated array items (may be null) + * Creates a new scope + * @return the previous, parent scope */ - public void setEvaluatedItems(Collection paths) { - this.evaluatedItems = null != paths ? paths : new ArrayList<>(); + public Scope enterDynamicScope() { + Scope parent = this.dynamicScopes.peek(); + this.dynamicScopes.push(newScope()); + return parent; } /** - * Replaces the array items that have been evaluated with an empty collection. + * Restores the previous, parent scope + * @return the exited scope */ - public void resetEvaluatedItems() { - this.evaluatedItems = new ArrayList<>(); + public Scope exitDynamicScope() { + return this.dynamicScopes.pop(); } /** - * Identifies which properties have been evaluated. - * - * @return the set of evaluated properties (never null) + * Provides the currently active scope + * @return the active scope */ - public Collection getEvaluatedProperties() { - return this.evaluatedProperties; + public Scope getDynamicScope() { + return this.dynamicScopes.peek(); } /** - * Set the properties that have been evaluated. - * @param paths the set of evaluated properties (may be null) + * Identifies which array items have been evaluated. + * + * @return the set of evaluated items (never null) */ - public void setEvaluatedProperties(Collection paths) { - this.evaluatedProperties = null != paths ? paths : new ArrayList<>(); + public Collection getEvaluatedItems() { + return getDynamicScope().getEvaluatedItems(); } /** - * Replaces the properties that have been evaluated with an empty collection. + * Identifies which properties have been evaluated. + * + * @return the set of evaluated properties (never null) */ - public void resetEvaluatedProperties() { - this.evaluatedProperties = new ArrayList<>(); + public Collection getEvaluatedProperties() { + return getDynamicScope().getEvaluatedProperties(); } /** @@ -181,8 +186,8 @@ public void combineWithCollector(String name, Object data) { public void reset() { this.collectorMap = new HashMap<>(); this.collectorLoadMap = new HashMap<>(); - this.evaluatedItems.clear(); - this.evaluatedProperties.clear(); + this.dynamicScopes.clear(); + this.dynamicScopes.push(newScope()); } /** @@ -199,4 +204,90 @@ void loadCollectors() { } + private Scope newScope() { + return new Scope(this.disableUnevaluatedItems, this.disableUnevaluatedProperties); + } + + public static class Scope { + + /** + * Used to track which array items have been evaluated. + */ + private final Collection evaluatedItems; + + /** + * Used to track which properties have been evaluated. + */ + private final Collection evaluatedProperties; + + Scope(boolean disableUnevaluatedItems, boolean disableUnevaluatedProperties) { + this.evaluatedItems = newCollection(disableUnevaluatedItems); + this.evaluatedProperties = newCollection(disableUnevaluatedProperties); + } + + private static Collection newCollection(boolean disabled) { + return !disabled ? new ArrayList<>() : new AbstractCollection() { + + @Override + public boolean add(String e) { + return false; + } + + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public boolean remove(Object o) { + return false; + } + + @Override + public int size() { + return 0; + } + + }; + } + + /** + * Identifies which array items have been evaluated. + * + * @return the set of evaluated items (never null) + */ + public Collection getEvaluatedItems() { + return this.evaluatedItems; + } + + /** + * Identifies which properties have been evaluated. + * + * @return the set of evaluated properties (never null) + */ + public Collection getEvaluatedProperties() { + return this.evaluatedProperties; + } + + /** + * Merges the provided scope into this scope. + * @param scope the scope to merge + * @return this scope + */ + public Scope mergeWith(Scope scope) { + getEvaluatedItems().addAll(scope.getEvaluatedItems()); + getEvaluatedProperties().addAll(scope.getEvaluatedProperties()); + return this; + } + + @Override + public String toString() { + return new StringBuilder("{ ") + .append("\"evaluatedItems\": ").append(this.evaluatedItems) + .append(", ") + .append("\"evaluatedProperties\": ").append(this.evaluatedProperties) + .append(" }").toString(); + } + + } } diff --git a/src/main/java/com/networknt/schema/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java index 44d103220..6a66cd308 100644 --- a/src/main/java/com/networknt/schema/IfValidator.java +++ b/src/main/java/com/networknt/schema/IfValidator.java @@ -17,6 +17,8 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.CollectorContext.Scope; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,25 +62,9 @@ public Set validate(JsonNode node, JsonNode rootNode, String debug(logger, node, rootNode, at); CollectorContext collectorContext = CollectorContext.getInstance(); - // As if-then-else might contain multiple schemas take a backup of evaluated stuff. - Collection backupEvaluatedItems = collectorContext.getEvaluatedItems(); - Collection backupEvaluatedProperties = collectorContext.getEvaluatedProperties(); - - Collection ifEvaluatedItems = Collections.emptyList(); - Collection ifEvaluatedProperties = Collections.emptyList(); - - Collection thenEvaluatedItems = Collections.emptyList(); - Collection thenEvaluatedProperties = Collections.emptyList(); - - Collection elseEvaluatedItems = Collections.emptyList(); - Collection elseEvaluatedProperties = Collections.emptyList(); - - // Make the evaluated lists empty. - collectorContext.resetEvaluatedItems(); - collectorContext.resetEvaluatedProperties(); - Set errors = new LinkedHashSet<>(); + Scope parentScope = collectorContext.enterDynamicScope(); boolean ifConditionPassed = false; try { try { @@ -88,48 +74,21 @@ public Set validate(JsonNode node, JsonNode rootNode, String // An exception means the condition failed ifConditionPassed = false; } - // Evaluated stuff from if. - ifEvaluatedItems = collectorContext.getEvaluatedItems(); - ifEvaluatedProperties = collectorContext.getEvaluatedProperties(); if (ifConditionPassed && this.thenSchema != null) { - - // Make the evaluated lists empty. - collectorContext.resetEvaluatedItems(); - collectorContext.resetEvaluatedProperties(); - errors.addAll(this.thenSchema.validate(node, rootNode, at)); - - // Collect the then evaluated stuff. - thenEvaluatedItems = collectorContext.getEvaluatedItems(); - thenEvaluatedProperties = collectorContext.getEvaluatedProperties(); - } else if (!ifConditionPassed && this.elseSchema != null) { - - // Make the evaluated lists empty. - collectorContext.resetEvaluatedItems(); - collectorContext.resetEvaluatedProperties(); + // discard ifCondition results + collectorContext.exitDynamicScope(); + collectorContext.enterDynamicScope(); errors.addAll(this.elseSchema.validate(node, rootNode, at)); - - // Collect the else evaluated stuff. - elseEvaluatedItems = collectorContext.getEvaluatedItems(); - elseEvaluatedProperties = collectorContext.getEvaluatedProperties(); } } finally { - collectorContext.setEvaluatedItems(backupEvaluatedItems); - collectorContext.setEvaluatedProperties(backupEvaluatedProperties); + Scope scope = collectorContext.exitDynamicScope(); if (errors.isEmpty()) { - // If the "if" keyword condition is passed then only add if stuff as evaluated. - if (ifConditionPassed) { - collectorContext.getEvaluatedItems().addAll(ifEvaluatedItems); - collectorContext.getEvaluatedProperties().addAll(ifEvaluatedProperties); - } - collectorContext.getEvaluatedItems().addAll(thenEvaluatedItems); - collectorContext.getEvaluatedItems().addAll(elseEvaluatedItems); - collectorContext.getEvaluatedProperties().addAll(thenEvaluatedProperties); - collectorContext.getEvaluatedProperties().addAll(elseEvaluatedProperties); + parentScope.mergeWith(scope); } } diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index ce41e9d3d..3e15b25bb 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.networknt.schema.CollectorContext.Scope; import com.networknt.schema.ValidationContext.DiscriminatorContext; import com.networknt.schema.utils.StringUtils; import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner; @@ -30,8 +31,6 @@ import java.net.URLDecoder; import java.text.MessageFormat; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * This is the core of json constraint implementation. It parses json constraint @@ -39,7 +38,6 @@ * constructed, it can be used to validate multiple json data concurrently. */ public class JsonSchema extends BaseJsonValidator { - private static final Pattern intPattern = Pattern.compile("^[0-9]+$"); private Map validators; private final JsonMetaSchema metaSchema; private boolean validatorsLoaded = false; @@ -195,27 +193,21 @@ public JsonNode getRefSchemaNode(String ref) { JsonSchema schema = findAncestor(); JsonNode node = schema.getSchemaNode(); - if (ref.startsWith("#/")) { - // handle local ref - String[] keys = ref.substring(2).split("/"); - for (String key : keys) { - try { - key = URLDecoder.decode(key, "utf-8"); - } catch (UnsupportedEncodingException e) { - // ignored - } - Matcher matcher = intPattern.matcher(key); - if (matcher.matches()) { - node = node.get(Integer.parseInt(key)); - } else { - node = node.get(key); - } - if (node == null) { - node = handleNullNode(ref, schema); - } - if (node == null) { - break; - } + String jsonPointer = ref; + if (jsonPointer.startsWith("#/")) { + jsonPointer = ref.substring(1); + } + + if (jsonPointer.startsWith("/")) { + try { + jsonPointer = URLDecoder.decode(jsonPointer, "utf-8"); + } catch (UnsupportedEncodingException e) { + // ignored + } + + node = node.at(jsonPointer); + if (node.isMissingNode()) { + node = handleNullNode(ref, schema); } } else if (ref.startsWith("#") && ref.length() > 1) { node = this.metaSchema.getNodeByFragmentRef(ref, node); @@ -223,6 +215,7 @@ public JsonNode getRefSchemaNode(String ref) { node = handleNullNode(ref, schema); } } + return node; } @@ -347,11 +340,29 @@ public Set validate(JsonNode jsonNode, JsonNode rootNode, Str SchemaValidatorsConfig config = this.validationContext.getConfig(); Set errors = new LinkedHashSet<>(); // Get the collector context. - getCollectorContext(); + CollectorContext collectorContext = getCollectorContext(); // Set the walkEnabled and isValidationEnabled flag in internal validator state. setValidatorState(false, true); for (JsonValidator v : getValidators().values()) { - errors.addAll(v.validate(jsonNode, rootNode, at)); + Set results = Collections.emptySet(); + + Scope parentScope = collectorContext.enterDynamicScope(); + try { + results = v.validate(jsonNode, rootNode, at); + } finally { + Scope scope = collectorContext.exitDynamicScope(); + if (results.isEmpty()) { + parentScope.mergeWith(scope); + } else { + errors.addAll(results); + if (v instanceof PrefixItemsValidator || v instanceof ItemsValidator || v instanceof ItemsValidator202012 || v instanceof ContainsValidator) { + collectorContext.getEvaluatedItems().addAll(scope.getEvaluatedItems()); + } + if (v instanceof PropertiesValidator || v instanceof AdditionalPropertiesValidator || v instanceof PatternPropertiesValidator) { + collectorContext.getEvaluatedProperties().addAll(scope.getEvaluatedProperties()); + } + } + } } if (null != config && config.isOpenAPI3StyleDiscriminators()) { @@ -521,19 +532,15 @@ private static void setValidatorState(boolean isWalkEnabled, boolean shouldValid } public CollectorContext getCollectorContext() { - SchemaValidatorsConfig config = this.validationContext.getConfig(); - CollectorContext collectorContext = (CollectorContext) ThreadInfo - .get(CollectorContext.COLLECTOR_CONTEXT_THREAD_LOCAL_KEY); - if (collectorContext == null) { - if (config != null && config.getCollectorContext() != null) { - collectorContext = config.getCollectorContext(); - } else { - collectorContext = new CollectorContext(); - } - // Set the collector context in thread info, this is unique for every thread. - ThreadInfo.set(CollectorContext.COLLECTOR_CONTEXT_THREAD_LOCAL_KEY, collectorContext); - } - return collectorContext; + return (CollectorContext) ThreadInfo.computeIfAbsent( + CollectorContext.COLLECTOR_CONTEXT_THREAD_LOCAL_KEY, + k -> Optional.ofNullable(this.validationContext.getConfig()) + .map(SchemaValidatorsConfig::getCollectorContext) + .orElseGet(() -> Optional.ofNullable(this.validationContext.getConfig()) + .map(config -> new CollectorContext(config.isUnevaluatedItemsAnalysisDisabled(), config.isUnevaluatedPropertiesAnalysisDisabled())) + .orElseGet(() -> new CollectorContext(false, false)) + ) + ); } @Override diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index a875436b7..2a5d09950 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -17,6 +17,8 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.CollectorContext.Scope; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,14 +41,7 @@ public Set validate(JsonNode node, JsonNode rootNode, String CollectorContext collectorContext = CollectorContext.getInstance(); Set errors = new HashSet<>(); - // As not will contain a schema take a backup of evaluated stuff. - Collection backupEvaluatedItems = collectorContext.getEvaluatedItems(); - Collection backupEvaluatedProperties = collectorContext.getEvaluatedProperties(); - - // Make the evaluated lists empty. - collectorContext.resetEvaluatedItems(); - collectorContext.resetEvaluatedProperties(); - + Scope parentScope = collectorContext.enterDynamicScope(); try { debug(logger, node, rootNode, at); errors = this.schema.validate(node, rootNode, at); @@ -55,12 +50,9 @@ public Set validate(JsonNode node, JsonNode rootNode, String } return Collections.emptySet(); } finally { + Scope scope = collectorContext.exitDynamicScope(); if (errors.isEmpty()) { - collectorContext.getEvaluatedItems().addAll(backupEvaluatedItems); - collectorContext.getEvaluatedProperties().addAll(backupEvaluatedProperties); - } else { - collectorContext.setEvaluatedItems(backupEvaluatedItems); - collectorContext.setEvaluatedProperties(backupEvaluatedProperties); + parentScope.mergeWith(scope); } } } diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 32a2cee6a..a44c17093 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -17,6 +17,8 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.CollectorContext.Scope; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,14 +44,7 @@ public Set validate(JsonNode node, JsonNode rootNode, String Set errors = new LinkedHashSet<>(); CollectorContext collectorContext = CollectorContext.getInstance(); - // As oneOf might contain multiple schemas take a backup of evaluated stuff. - Collection backupEvaluatedItems = collectorContext.getEvaluatedItems(); - Collection backupEvaluatedProperties = collectorContext.getEvaluatedProperties(); - - // Make the evaluated lists empty. - collectorContext.resetEvaluatedItems(); - collectorContext.resetEvaluatedProperties(); - + Scope grandParentScope = collectorContext.enterDynamicScope(); try { debug(logger, node, rootNode, at); @@ -62,33 +57,40 @@ public Set validate(JsonNode node, JsonNode rootNode, String Set childErrors = new LinkedHashSet<>(); for (JsonSchema schema : this.schemas) { - Set schemaErrors = null; - // Reset state in case the previous validator did not match - state.setMatchedNode(true); - - if (!state.isWalkEnabled()) { - schemaErrors = schema.validate(node, rootNode, at); - } else { - schemaErrors = schema.walk(node, rootNode, at, state.isValidationEnabled()); + Set schemaErrors = Collections.emptySet(); + + Scope parentScope = collectorContext.enterDynamicScope(); + try { + // Reset state in case the previous validator did not match + state.setMatchedNode(true); + + if (!state.isWalkEnabled()) { + schemaErrors = schema.validate(node, rootNode, at); + } else { + schemaErrors = schema.walk(node, rootNode, at, state.isValidationEnabled()); + } + + // check if any validation errors have occurred + if (schemaErrors.isEmpty()) { + // check whether there are no errors HOWEVER we have validated the exact validator + if (!state.hasMatchedNode()) + continue; + + numberOfValidSchema++; + } + + if (numberOfValidSchema > 1) { + // short-circuit + break; + } + + childErrors.addAll(schemaErrors); + } finally { + Scope scope = collectorContext.exitDynamicScope(); + if (schemaErrors.isEmpty()) { + parentScope.mergeWith(scope); + } } - - // check if any validation errors have occurred - if (schemaErrors.isEmpty()) { - // check whether there are no errors HOWEVER we have validated the exact validator - if (!state.hasMatchedNode()) - continue; - - numberOfValidSchema++; - } - - // If the number of valid schema is greater than one, just reset the evaluated properties and break. - if (numberOfValidSchema > 1) { - collectorContext.resetEvaluatedItems(); - collectorContext.resetEvaluatedProperties(); - break; - } - - childErrors.addAll(schemaErrors); } // ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1. @@ -99,6 +101,8 @@ public Set validate(JsonNode node, JsonNode rootNode, String } errors.add(message); errors.addAll(childErrors); + collectorContext.getEvaluatedItems().clear(); + collectorContext.getEvaluatedProperties().clear(); } // Make sure to signal parent handlers we matched @@ -110,12 +114,9 @@ public Set validate(JsonNode node, JsonNode rootNode, String return Collections.unmodifiableSet(errors); } finally { + Scope scope = collectorContext.exitDynamicScope(); if (errors.isEmpty()) { - collectorContext.getEvaluatedItems().addAll(backupEvaluatedItems); - collectorContext.getEvaluatedProperties().addAll(backupEvaluatedProperties); - } else { - collectorContext.setEvaluatedItems(backupEvaluatedItems); - collectorContext.setEvaluatedProperties(backupEvaluatedProperties); + grandParentScope.mergeWith(scope); } } } diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java index 389d810fa..384071b58 100644 --- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java @@ -57,8 +57,11 @@ public Set validate(JsonNode node, JsonNode rootNode, String JsonNode n = node.get(name); for (Map.Entry entry : schemas.entrySet()) { if (entry.getKey().matches(name)) { - CollectorContext.getInstance().getEvaluatedProperties().add(atPath(at, name)); - errors.addAll(entry.getValue().validate(n, rootNode, atPath(at, name))); + Set results = entry.getValue().validate(n, rootNode, atPath(at, name)); + if (results.isEmpty()) { + CollectorContext.getInstance().getEvaluatedProperties().add(atPath(at, name)); + } + errors.addAll(results); } } } diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index 9aaa9d4b4..1a85d6c1f 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -55,7 +55,7 @@ public Set validate(JsonNode node, JsonNode rootNode, String JsonSchema propertySchema = entry.getValue(); JsonNode propertyNode = node.get(entry.getKey()); if (propertyNode != null) { - collectorContext.getEvaluatedProperties().add(atPath(at, entry.getKey())); + collectorContext.getEvaluatedProperties().add(atPath(at, entry.getKey())); // TODO: This should happen after validation // 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 diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 075b85bde..2867e996e 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -17,6 +17,7 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.CollectorContext.Scope; import com.networknt.schema.uri.URIFactory; import com.networknt.schema.urn.URNFactory; import org.slf4j.Logger; @@ -39,8 +40,8 @@ public RefValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSch super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.REF, validationContext); String refValue = schemaNode.asText(); this.parentSchema = parentSchema; - schema = getRefSchema(parentSchema, validationContext, refValue); - if (schema == null) { + this.schema = getRefSchema(parentSchema, validationContext, refValue); + if (this.schema == null) { throw new JsonSchemaException( ValidationMessage.of( ValidatorTypeCode.REF.getValue(), @@ -53,6 +54,7 @@ public RefValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSch static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue) { final String refValueOriginal = refValue; + JsonSchema parent = parentSchema; if (!refValue.startsWith(REF_CURRENT)) { // This will be the uri extracted from the refValue (this may be a relative or absolute uri). final String refUri; @@ -65,7 +67,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val // This will determine the correct absolute uri for the refUri. This decision will take into // account the current uri of the parent schema. - URI schemaUri = determineSchemaUri(validationContext.getURIFactory(), parentSchema, refUri); + URI schemaUri = determineSchemaUri(validationContext.getURIFactory(), parent, refUri); if (schemaUri == null) { // the URNFactory is optional if (validationContext.getURNFactory() == null) { @@ -79,21 +81,21 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val } // This should retrieve schemas regardless of the protocol that is in the uri. - parentSchema = validationContext.getJsonSchemaFactory().getSchema(schemaUri, validationContext.getConfig()); + parent = validationContext.getJsonSchemaFactory().getSchema(schemaUri, validationContext.getConfig()); if (index < 0) { - return new JsonSchemaRef(parentSchema.findAncestor()); + return new JsonSchemaRef(parent.findAncestor()); } refValue = refValue.substring(index); } if (refValue.equals(REF_CURRENT)) { - return new JsonSchemaRef(parentSchema.findAncestor()); + return new JsonSchemaRef(parent.findAncestor()); } - JsonNode node = parentSchema.getRefSchemaNode(refValue); + JsonNode node = parent.getRefSchemaNode(refValue); if (node != null) { JsonSchemaRef ref = validationContext.getReferenceParsingInProgress(refValueOriginal); if (ref == null) { - final JsonSchema schema = validationContext.newSchema(refValue, node, parentSchema); + final JsonSchema schema = validationContext.newSchema(refValue, node, parent); ref = new JsonSchemaRef(schema); validationContext.setReferenceParsingInProgress(refValueOriginal, ref); } @@ -127,33 +129,28 @@ private static URI determineSchemaUrn(final URNFactory urnFactory, final String return schemaUrn; } + @Override public Set validate(JsonNode node, JsonNode rootNode, String at) { CollectorContext collectorContext = CollectorContext.getInstance(); Set errors = new HashSet<>(); - // As ref will contain a schema take a backup of evaluatedProperties. - Collection backupEvaluatedProperties = collectorContext.getEvaluatedProperties(); - - // Make the evaluatedProperties list empty. - collectorContext.resetEvaluatedProperties(); - + Scope parentScope = collectorContext.enterDynamicScope(); try { debug(logger, node, rootNode, at); // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances, // these schemas will be cached along with config. We have to replace the config for cached $ref references // with the latest config. Reset the config. - schema.getSchema().getValidationContext().setConfig(parentSchema.getValidationContext().getConfig()); - if (schema != null) { - errors = schema.validate(node, rootNode, at); + this.schema.getSchema().getValidationContext().setConfig(this.parentSchema.getValidationContext().getConfig()); + if (this.schema != null) { + errors = this.schema.validate(node, rootNode, at); } else { errors = Collections.emptySet(); } } finally { + Scope scope = collectorContext.exitDynamicScope(); if (errors.isEmpty()) { - collectorContext.getEvaluatedProperties().addAll(backupEvaluatedProperties); - } else { - collectorContext.setEvaluatedProperties(backupEvaluatedProperties); + parentScope.mergeWith(scope); } } return errors; @@ -165,40 +162,34 @@ public Set walk(JsonNode node, JsonNode rootNode, String at, Set errors = new HashSet<>(); - // As ref will contain a schema take a backup of evaluatedProperties. - Collection backupEvaluatedProperties = collectorContext.getEvaluatedProperties(); - - // Make the evaluatedProperties list empty. - collectorContext.resetEvaluatedProperties(); - + Scope parentScope = collectorContext.enterDynamicScope(); try { debug(logger, node, rootNode, at); // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances, // these schemas will be cached along with config. We have to replace the config for cached $ref references // with the latest config. Reset the config. - schema.getSchema().getValidationContext().setConfig(parentSchema.getValidationContext().getConfig()); - if (schema != null) { - errors = schema.walk(node, rootNode, at, shouldValidateSchema); + this.schema.getSchema().getValidationContext().setConfig(this.parentSchema.getValidationContext().getConfig()); + if (this.schema != null) { + errors = this.schema.walk(node, rootNode, at, shouldValidateSchema); } return errors; } finally { + Scope scope = collectorContext.exitDynamicScope(); if (shouldValidateSchema) { if (errors.isEmpty()) { - collectorContext.getEvaluatedProperties().addAll(backupEvaluatedProperties); - } else { - collectorContext.setEvaluatedProperties(backupEvaluatedProperties); + parentScope.mergeWith(scope); } } } } public JsonSchemaRef getSchemaRef() { - return schema; + return this.schema; } @Override public void preloadJsonSchema() { - schema.getSchema().initializeValidators(); + this.schema.getSchema().initializeValidators(); } } diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index 5f4262bb7..044a8f91a 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -87,7 +87,7 @@ public class SchemaValidatorsConfig { * URLs. This is merged with any mappings the {@link JsonSchemaFactory} may have * been built with. */ - private Map uriMappings = new HashMap(); + private Map uriMappings = new HashMap<>(); private CompositeURITranslator uriTranslators = new CompositeURITranslator(); @@ -122,11 +122,11 @@ public class SchemaValidatorsConfig { // This is just a constant for listening to all Keywords. public static final String ALL_KEYWORD_WALK_LISTENER_KEY = "com.networknt.AllKeywordWalkListener"; - private final Map> keywordWalkListenersMap = new HashMap>(); + private final Map> keywordWalkListenersMap = new HashMap<>(); - private final List propertyWalkListeners = new ArrayList(); + private final List propertyWalkListeners = new ArrayList<>(); - private final List itemWalkListeners = new ArrayList(); + private final List itemWalkListeners = new ArrayList<>(); private CollectorContext collectorContext; @@ -144,8 +144,64 @@ public class SchemaValidatorsConfig { private ResourceBundle resourceBundle; private ResourceBundle resourceBundleToUse; + /************************ START OF UNEVALUATED CHECKS **********************************/ + + // These are costly in terms of performance so we provide a way to disable them. + private boolean disableUnevaluatedItems = false; + private boolean disableUnevaluatedProperties = false; + + public SchemaValidatorsConfig disableUnevaluatedAnalysis() { + disableUnevaluatedItems(); + disableUnevaluatedProperties(); + return this; + } + + public SchemaValidatorsConfig disableUnevaluatedItems() { + this.disableUnevaluatedItems = true; + return this; + } + + public SchemaValidatorsConfig disableUnevaluatedProperties() { + this.disableUnevaluatedProperties = true; + return this; + } + + public SchemaValidatorsConfig enableUnevaluatedAnalysis() { + enableUnevaluatedItems(); + enableUnevaluatedProperties(); + return this; + } + + public SchemaValidatorsConfig enableUnevaluatedItems() { + this.disableUnevaluatedItems = false; + return this; + } + + public SchemaValidatorsConfig enableUnevaluatedProperties() { + this.disableUnevaluatedProperties = false; + return this; + } + + public boolean isUnevaluatedItemsAnalysisDisabled() { + return this.disableUnevaluatedItems; + } + + public boolean isUnevaluatedItemsAnalysisEnabled() { + return !isUnevaluatedItemsAnalysisDisabled(); + } + + public boolean isUnevaluatedPropertiesAnalysisDisabled() { + return this.disableUnevaluatedProperties; + } + + public boolean isUnevaluatedPropertiesAnalysisEnabled() { + return !isUnevaluatedPropertiesAnalysisDisabled(); + } + + /************************ END OF UNEVALUATED CHECKS **********************************/ + public boolean isTypeLoose() { - return typeLoose; + return this.typeLoose; } public void setTypeLoose(boolean typeLoose) { @@ -174,7 +230,7 @@ public void setApplyDefaultsStrategy(ApplyDefaultsStrategy applyDefaultsStrategy } public ApplyDefaultsStrategy getApplyDefaultsStrategy() { - return applyDefaultsStrategy; + return this.applyDefaultsStrategy; } public CompositeURITranslator getUriTranslator() { @@ -195,7 +251,7 @@ public void addUriTranslator(URITranslator uriTranslator) { @Deprecated public Map getUriMappings() { // return a copy of the mappings - return new HashMap(uriMappings); + return new HashMap<>(this.uriMappings); } /** @@ -208,7 +264,7 @@ public void setUriMappings(Map uriMappings) { } public boolean isHandleNullableField() { - return handleNullableField; + return this.handleNullableField; } public void setHandleNullableField(boolean handleNullableField) { @@ -216,7 +272,7 @@ public void setHandleNullableField(boolean handleNullableField) { } public boolean isEcma262Validator() { - return ecma262Validator; + return this.ecma262Validator; } public void setEcma262Validator(boolean ecma262Validator) { @@ -224,7 +280,7 @@ public void setEcma262Validator(boolean ecma262Validator) { } public boolean isJavaSemantics() { - return javaSemantics; + return this.javaSemantics; } public void setJavaSemantics(boolean javaSemantics) { @@ -240,35 +296,35 @@ public void setCustomMessageSupported(boolean customMessageSupported) { } public void addKeywordWalkListener(JsonSchemaWalkListener keywordWalkListener) { - if (keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) { - List keywordWalkListeners = new ArrayList(); - keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, keywordWalkListeners); + if (this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) { + List keywordWalkListeners = new ArrayList<>(); + this.keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, keywordWalkListeners); } - keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).add(keywordWalkListener); + this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).add(keywordWalkListener); } public void addKeywordWalkListener(String keyword, JsonSchemaWalkListener keywordWalkListener) { - if (keywordWalkListenersMap.get(keyword) == null) { - List keywordWalkListeners = new ArrayList(); - keywordWalkListenersMap.put(keyword, keywordWalkListeners); + if (this.keywordWalkListenersMap.get(keyword) == null) { + List keywordWalkListeners = new ArrayList<>(); + this.keywordWalkListenersMap.put(keyword, keywordWalkListeners); } - keywordWalkListenersMap.get(keyword).add(keywordWalkListener); + this.keywordWalkListenersMap.get(keyword).add(keywordWalkListener); } public void addKeywordWalkListeners(List keywordWalkListeners) { - if (keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) { - List ikeywordWalkListeners = new ArrayList(); - keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, ikeywordWalkListeners); + if (this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) { + List ikeywordWalkListeners = new ArrayList<>(); + this.keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, ikeywordWalkListeners); } - keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).addAll(keywordWalkListeners); + this.keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).addAll(keywordWalkListeners); } public void addKeywordWalkListeners(String keyword, List keywordWalkListeners) { - if (keywordWalkListenersMap.get(keyword) == null) { - List ikeywordWalkListeners = new ArrayList(); - keywordWalkListenersMap.put(keyword, ikeywordWalkListeners); + if (this.keywordWalkListenersMap.get(keyword) == null) { + List ikeywordWalkListeners = new ArrayList<>(); + this.keywordWalkListenersMap.put(keyword, ikeywordWalkListeners); } - keywordWalkListenersMap.get(keyword).addAll(keywordWalkListeners); + this.keywordWalkListenersMap.get(keyword).addAll(keywordWalkListeners); } public void addPropertyWalkListeners(List propertyWalkListeners) { @@ -303,7 +359,7 @@ public SchemaValidatorsConfig() { } public CollectorContext getCollectorContext() { - return collectorContext; + return this.collectorContext; } public void setCollectorContext(CollectorContext collectorContext) { @@ -311,7 +367,7 @@ public void setCollectorContext(CollectorContext collectorContext) { } public boolean isLosslessNarrowing() { - return losslessNarrowing; + return this.losslessNarrowing; } public void setLosslessNarrowing(boolean losslessNarrowing) { @@ -325,7 +381,7 @@ public void setLosslessNarrowing(boolean losslessNarrowing) { * @since 1.0.51 */ public boolean isOpenAPI3StyleDiscriminators() { - return openAPI3StyleDiscriminators; + return this.openAPI3StyleDiscriminators; } /** @@ -370,11 +426,11 @@ public void setLoadCollectors(boolean loadCollectors) { } public boolean doLoadCollectors() { - return loadCollectors; + return this.loadCollectors; } public boolean isResetCollectorContext() { - return resetCollectorContext; + return this.resetCollectorContext; } public void setResetCollectorContext(boolean resetCollectorContext) { @@ -382,7 +438,7 @@ public void setResetCollectorContext(boolean resetCollectorContext) { } public boolean isWriteMode() { - return writeMode; + return this.writeMode; } /** @@ -410,7 +466,7 @@ public void setPathType(PathType pathType) { * @return The path generation approach. */ public PathType getPathType() { - return pathType; + return this.pathType; } /** @@ -446,7 +502,7 @@ public void setStrict(String keyword, boolean strict) { * @return The locale. */ public Locale getLocale() { - return locale; + return this.locale; } /** @@ -468,18 +524,18 @@ public void setLocale(Locale locale) { * @return The resource bundle. */ public ResourceBundle getResourceBundle() { - if (resourceBundleToUse == null) { + if (this.resourceBundleToUse == null) { // Load and cache the resource bundle to use. - resourceBundleToUse = resourceBundle; - if (resourceBundleToUse == null) { - if (locale == null) { - resourceBundleToUse = I18nSupport.DEFAULT_RESOURCE_BUNDLE; + this.resourceBundleToUse = this.resourceBundle; + if (this.resourceBundleToUse == null) { + if (this.locale == null) { + this.resourceBundleToUse = I18nSupport.DEFAULT_RESOURCE_BUNDLE; } else { - resourceBundleToUse = ResourceBundle.getBundle(I18nSupport.DEFAULT_BUNDLE_BASE_NAME, locale); + this.resourceBundleToUse = ResourceBundle.getBundle(I18nSupport.DEFAULT_BUNDLE_BASE_NAME, this.locale); } } } - return resourceBundleToUse; + return this.resourceBundleToUse; } /** diff --git a/src/main/java/com/networknt/schema/ThreadInfo.java b/src/main/java/com/networknt/schema/ThreadInfo.java index e09a86c62..d0a98b4f0 100644 --- a/src/main/java/com/networknt/schema/ThreadInfo.java +++ b/src/main/java/com/networknt/schema/ThreadInfo.java @@ -17,21 +17,25 @@ import java.util.HashMap; import java.util.Map; +import java.util.function.Function; public class ThreadInfo { private static ThreadLocal> threadLocal = new ThreadLocal>() { + @Override protected java.util.Map initialValue() { - return new HashMap(); + return new HashMap<>(); } - - ; }; public static Object get(String key) { return threadLocal.get().get(key); } + public static Object computeIfAbsent(String key, Function mappingFunction) { + return threadLocal.get().computeIfAbsent(key, mappingFunction); + } + public static void set(String key, Object value) { Map threadLocalMap = threadLocal.get(); threadLocalMap.put(key, value); diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index 38abd0d15..3e20d1c72 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -32,39 +32,46 @@ public class TypeValidator extends BaseJsonValidator { public TypeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.TYPE, validationContext); - schemaType = TypeFactory.getSchemaNodeType(schemaNode); + this.schemaType = TypeFactory.getSchemaNodeType(schemaNode); this.parentSchema = parentSchema; this.validationContext = validationContext; - if (schemaType == JsonType.UNION) { - unionTypeValidator = new UnionTypeValidator(schemaPath, schemaNode, parentSchema, validationContext); + if (this.schemaType == JsonType.UNION) { + this.unionTypeValidator = new UnionTypeValidator(schemaPath, schemaNode, parentSchema, validationContext); } parseErrorCode(getValidatorType().getErrorCodeKey()); } public JsonType getSchemaType() { - return schemaType; + return this.schemaType; } public boolean equalsToSchemaType(JsonNode node) { - return JsonNodeUtil.equalsToSchemaType(node,schemaType, parentSchema, validationContext); + return JsonNodeUtil.equalsToSchemaType(node,this.schemaType, this.parentSchema, this.validationContext); } + @Override public Set validate(JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); - if (schemaType == JsonType.UNION) { - return unionTypeValidator.validate(node, rootNode, at); + if (this.schemaType == JsonType.UNION) { + return this.unionTypeValidator.validate(node, rootNode, at); } if (!equalsToSchemaType(node)) { - JsonType nodeType = TypeFactory.getValueNodeType(node, validationContext.getConfig()); - return Collections.singleton(buildValidationMessage(at, nodeType.toString(), schemaType.toString())); + JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); + return Collections.singleton(buildValidationMessage(at, nodeType.toString(), this.schemaType.toString())); } + + // TODO: Is this really necessary? // Hack to catch evaluated properties if additionalProperties is given as "additionalProperties":{"type":"string"} // Hack to catch patternProperties like "^foo":"value" - if (schemaPath.endsWith("/type")) { - CollectorContext.getInstance().getEvaluatedProperties().add(at); + if (this.schemaPath.endsWith("/type")) { + if (rootNode.isArray()) { + CollectorContext.getInstance().getEvaluatedItems().add(at); + } else if (rootNode.isObject()) { + CollectorContext.getInstance().getEvaluatedProperties().add(at); + } } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 53538d9c8..b072accf6 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -21,20 +21,25 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.utils.JsonNodeUtil; public class UnevaluatedItemsValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnevaluatedItemsValidator.class); private final JsonSchema schema; + private final boolean disabled; public UnevaluatedItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_ITEMS, validationContext); + this.disabled = validationContext.getConfig().isUnevaluatedItemsAnalysisDisabled(); if (schemaNode.isObject() || schemaNode.isBoolean()) { this.schema = validationContext.newSchema(schemaPath, schemaNode, parentSchema); } else { @@ -44,38 +49,70 @@ public UnevaluatedItemsValidator(String schemaPath, JsonNode schemaNode, JsonSch @Override public Set validate(JsonNode node, JsonNode rootNode, String at) { + if (this.disabled) return Collections.emptySet(); + debug(logger, node, rootNode, at); CollectorContext collectorContext = CollectorContext.getInstance(); - Set allPaths = allPaths(node, at); - Set unevaluatedPaths = unevaluatedPaths(allPaths); + collectorContext.exitDynamicScope(); + try { + Set allPaths = allPaths(node, at); - Set failingPaths = new HashSet<>(); - unevaluatedPaths.forEach(path -> { - String pointer = getPathType().convertToJsonPointer(path); - JsonNode property = rootNode.at(pointer); - if (!this.schema.validate(property, rootNode, path).isEmpty()) { - failingPaths.add(path); + // Short-circuit since schema is 'true' + if (super.schemaNode.isBoolean() && super.schemaNode.asBoolean()) { + collectorContext.getEvaluatedItems().addAll(allPaths); + return Collections.emptySet(); } - }); - if (failingPaths.isEmpty()) { - collectorContext.getEvaluatedItems().addAll(allPaths); - } else { - List paths = new ArrayList<>(failingPaths); - paths.sort(String.CASE_INSENSITIVE_ORDER); - return Collections.singleton(buildValidationMessage(String.join(", ", paths))); - } + Set unevaluatedPaths = unevaluatedPaths(allPaths); + + // Short-circuit since schema is 'false' + if (super.schemaNode.isBoolean() && !super.schemaNode.asBoolean() && !unevaluatedPaths.isEmpty()) { + return reportUnevaluatedPaths(unevaluatedPaths); + } + + Set failingPaths = new HashSet<>(); + unevaluatedPaths.forEach(path -> { + String pointer = getPathType().convertToJsonPointer(path); + JsonNode property = rootNode.at(pointer); + if (!this.schema.validate(property, rootNode, path).isEmpty()) { + failingPaths.add(path); + } + }); + + if (failingPaths.isEmpty()) { + collectorContext.getEvaluatedItems().addAll(allPaths); + } else { + return reportUnevaluatedPaths(failingPaths); + } - return Collections.emptySet(); + return Collections.emptySet(); + } finally { + collectorContext.enterDynamicScope(); + } } + private static final Pattern NUMERIC = Pattern.compile("^\\d+$"); + private Set allPaths(JsonNode node, String at) { - Set results = new HashSet<>(); - for (int i = 0; i < node.size(); ++i) { - results.add(atPath(at, i)); - } - return results; + return JsonNodeUtil.allPaths(getPathType(), at, node) + .stream() + .filter(this::isArray) + .collect(Collectors.toSet()); + } + + private boolean isArray(String path) { + String jsonPointer = getPathType().convertToJsonPointer(path); + String[] segment = jsonPointer.split("/"); + if (0 == segment.length) return false; + String lastSegment = segment[segment.length - 1]; + return NUMERIC.matcher(lastSegment).matches(); + } + + private Set reportUnevaluatedPaths(Set unevaluatedPaths) { + List paths = new ArrayList<>(unevaluatedPaths); + paths.sort(String.CASE_INSENSITIVE_ORDER); + return Collections.singleton(buildValidationMessage(String.join("\n ", paths))); } private static Set unevaluatedPaths(Set allPaths) { diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index 5e05e1310..bac597486 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -17,21 +17,25 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.utils.JsonNodeUtil; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; public class UnevaluatedPropertiesValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnevaluatedPropertiesValidator.class); - public static final String UNEVALUATED_PROPERTIES = "com.networknt.schema.UnevaluatedPropertiesValidator.UnevaluatedProperties"; - private final JsonSchema schema; + private final boolean disabled; public UnevaluatedPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_PROPERTIES, validationContext); + this.disabled = validationContext.getConfig().isUnevaluatedPropertiesAnalysisDisabled(); if (schemaNode.isObject() || schemaNode.isBoolean()) { this.schema = validationContext.newSchema(schemaPath, schemaNode, parentSchema); } else { @@ -41,58 +45,75 @@ public UnevaluatedPropertiesValidator(String schemaPath, JsonNode schemaNode, Js @Override public Set validate(JsonNode node, JsonNode rootNode, String at) { + if (this.disabled) return Collections.emptySet(); + debug(logger, node, rootNode, at); CollectorContext collectorContext = CollectorContext.getInstance(); - Set allPaths = allPaths(node, at); - Set unevaluatedPaths = unevaluatedPaths(allPaths); + collectorContext.exitDynamicScope(); + try { + Set allPaths = allPaths(node, at); - Set failingPaths = new HashSet<>(); - unevaluatedPaths.forEach(path -> { - String pointer = getPathType().convertToJsonPointer(path); - JsonNode property = rootNode.at(pointer); - if (!this.schema.validate(property, rootNode, path).isEmpty()) { - failingPaths.add(path); + // Short-circuit since schema is 'true' + if (super.schemaNode.isBoolean() && super.schemaNode.asBoolean()) { + collectorContext.getEvaluatedProperties().addAll(allPaths); + return Collections.emptySet(); } - }); - if (failingPaths.isEmpty()) { - collectorContext.getEvaluatedProperties().addAll(allPaths); - } else { - // TODO: Why add this to the context if it is never referenced? - collectorContext.add(UNEVALUATED_PROPERTIES, unevaluatedPaths); - List paths = new ArrayList<>(failingPaths); - paths.sort(String.CASE_INSENSITIVE_ORDER); - return Collections.singleton(buildValidationMessage(String.join(", ", paths))); - } + Set unevaluatedPaths = unevaluatedPaths(allPaths); - return Collections.emptySet(); - } + // Short-circuit since schema is 'false' + if (super.schemaNode.isBoolean() && !super.schemaNode.asBoolean() && !unevaluatedPaths.isEmpty()) { + return reportUnevaluatedPaths(unevaluatedPaths); + } - private static Set unevaluatedPaths(Set allPaths) { - Set unevaluatedProperties = new HashSet<>(allPaths); - unevaluatedProperties.removeAll(CollectorContext.getInstance().getEvaluatedProperties()); - return unevaluatedProperties; + Set failingPaths = new HashSet<>(); + unevaluatedPaths.forEach(path -> { + String pointer = getPathType().convertToJsonPointer(path); + JsonNode property = rootNode.at(pointer); + if (!this.schema.validate(property, rootNode, path).isEmpty()) { + failingPaths.add(path); + } + }); + + if (failingPaths.isEmpty()) { + collectorContext.getEvaluatedProperties().addAll(allPaths); + } else { + return reportUnevaluatedPaths(failingPaths); + } + + return Collections.emptySet(); + } finally { + collectorContext.enterDynamicScope(); + } } + private static final Pattern NUMERIC = Pattern.compile("^\\d+$"); + private Set allPaths(JsonNode node, String at) { - Set results = new HashSet<>(); - processAllPaths(node, at, results); - return results; + return JsonNodeUtil.allPaths(getPathType(), at, node) + .stream() + .filter(this::isProperty) + .collect(Collectors.toSet()); } - private void processAllPaths(JsonNode node, String at, Set paths) { - Iterator nodesIterator = node.fieldNames(); - while (nodesIterator.hasNext()) { - String fieldName = nodesIterator.next(); - String path = atPath(at, fieldName); - paths.add(path); + private boolean isProperty(String path) { + String jsonPointer = getPathType().convertToJsonPointer(path); + String[] segment = jsonPointer.split("/"); + if (0 == segment.length) return false; + String lastSegment = segment[segment.length - 1]; + return !NUMERIC.matcher(lastSegment).matches(); + } - JsonNode jsonNode = node.get(fieldName); - if (jsonNode.isObject()) { - processAllPaths(jsonNode, path, paths); - } - } + private Set reportUnevaluatedPaths(Set unevaluatedPaths) { + List paths = new ArrayList<>(unevaluatedPaths); + paths.sort(String.CASE_INSENSITIVE_ORDER); + return Collections.singleton(buildValidationMessage(String.join("\n ", paths))); } + private static Set unevaluatedPaths(Set allPaths) { + Set unevaluatedProperties = new HashSet<>(allPaths); + unevaluatedProperties.removeAll(CollectorContext.getInstance().getEvaluatedProperties()); + return unevaluatedProperties; + } } diff --git a/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java b/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java index 99fd6342c..fc403df5e 100644 --- a/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java +++ b/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java @@ -1,8 +1,12 @@ package com.networknt.schema.utils; +import java.util.ArrayList; +import java.util.Collection; + import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonType; +import com.networknt.schema.PathType; import com.networknt.schema.SchemaValidatorsConfig; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.SpecVersionDetector; @@ -17,6 +21,37 @@ public class JsonNodeUtil { private static final String REF = "$ref"; private static final String NULLABLE = "nullable"; + public static Collection allPaths(PathType pathType, String root, JsonNode node) { + Collection collector = new ArrayList<>(); + visitNode(pathType, root, node, collector); + return collector; + } + + private static void visitNode(PathType pathType, String root, JsonNode node, Collection collector) { + if (node.isObject()) { + visitObject(pathType, root, node, collector); + } else if (node.isArray()) { + visitArray(pathType, root, node, collector); + } + } + + private static void visitArray(PathType pathType, String root, JsonNode node, Collection collector) { + int size = node.size(); + for (int i = 0; i < size; ++i) { + String path = pathType.append(root, i); + collector.add(path); + visitNode(pathType, path, node.get(i), collector); + } + } + + private static void visitObject(PathType pathType, String root, JsonNode node, Collection collector) { + node.fields().forEachRemaining(entry -> { + String path = pathType.append(root, entry.getKey()); + collector.add(path); + visitNode(pathType, path, entry.getValue(), collector); + }); + } + public static boolean isNodeNullable(JsonNode schema){ JsonNode nullable = schema.get(NULLABLE); if (nullable != null && nullable.asBoolean()) { diff --git a/src/main/resources/jsv-messages.properties b/src/main/resources/jsv-messages.properties index 8646eb370..99475125a 100644 --- a/src/main/resources/jsv-messages.properties +++ b/src/main/resources/jsv-messages.properties @@ -42,7 +42,7 @@ propertyNames = Property name {0} is not valid for validation: {1} readOnly = {0}: is a readonly field, it cannot be changed required = {0}.{1}: is missing but it is required type = {0}: {1} found, {2} expected -unevaluatedItems = There are unevaluated properties at the following paths {0} +unevaluatedItems = There are unevaluated items at the following paths {0} unevaluatedProperties = There are unevaluated properties at the following paths {0} unionType = {0}: {1} found, but {2} is required uniqueItems = {0}: the items in the array must be unique diff --git a/src/main/resources/jsv-messages_de.properties b/src/main/resources/jsv-messages_de.properties index 610a2bb19..4562a01a0 100644 --- a/src/main/resources/jsv-messages_de.properties +++ b/src/main/resources/jsv-messages_de.properties @@ -36,6 +36,7 @@ propertyNames = Eigenschaftsname {0} ist ung readOnly = {0} ist ein schreibgesch�tztes Feld und kann nicht ver�ndert werden required = {0}.{1} ist ein Pflichtfeld aber fehlt type = {0}: {1} wurde gefunden, aber {2} erwartet +unevaluatedItems = Elemente in den folgenden Pfaden wurden nicht bewertet: {0} unevaluatedProperties = Eigenschaften in folgenden Pfaden wurden nicht evaluiert: {0} unionType = {0}: {1} wurde gefunden aber {2} wird verlangt uniqueItems = {0}: Die Element(e) des Arrays d�rfen nur einmal auftreten diff --git a/src/test/java/com/networknt/schema/UriMappingTest.java b/src/test/java/com/networknt/schema/UriMappingTest.java index 33b966b13..0e55f9f41 100644 --- a/src/test/java/com/networknt/schema/UriMappingTest.java +++ b/src/test/java/com/networknt/schema/UriMappingTest.java @@ -81,11 +81,11 @@ public void testBuilderExampleMappings() throws IOException { } catch (JsonSchemaException ex) { Throwable cause = ex.getCause(); if (!(cause instanceof FileNotFoundException || cause instanceof UnknownHostException)) { - fail("Unexpected cause for JsonSchemaException"); + fail("Unexpected cause for JsonSchemaException", ex); } // passing, so do nothing } catch (Exception ex) { - fail("Unexpected exception thrown"); + fail("Unexpected exception thrown", ex); } URL mappings = ClasspathURLFactory.convert( this.classpathURLFactory.create("resource:draft4/extra/uri_mapping/invalid-schema-uri.json")); diff --git a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json index eccbfd898..1103bc7d9 100644 --- a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json +++ b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json @@ -233,7 +233,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.vehicle.pontoons, $.vehicle.wings" + "There are unevaluated properties at the following paths $.vehicle.pontoons\n $.vehicle.wings" ] }, { @@ -250,7 +250,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.vehicle.invalid, $.vehicle.pontoons, $.vehicle.wings" + "There are unevaluated properties at the following paths $.vehicle.invalid\n $.vehicle.pontoons\n $.vehicle.wings" ] }, { @@ -391,7 +391,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.vehicle.unevaluated, $.vehicle.wings" + "There are unevaluated properties at the following paths $.vehicle.unevaluated\n $.vehicle.wings" ] } ] @@ -488,7 +488,7 @@ "valid": false, "validationMessages": [ "$.vehicle.wings: is missing but it is required", - "There are unevaluated properties at the following paths $.vehicle.unevaluated" + "There are unevaluated properties at the following paths $.vehicle.pontoons\n $.vehicle.unevaluated\n $.vehicle.wheels" ] } ] @@ -550,7 +550,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.age, $.unevaluated" + "There are unevaluated properties at the following paths $.age\n $.unevaluated" ] } ]