diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index 8442b47d6..dc2091c13 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.networknt.schema.utils.SetView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,7 +49,7 @@ public Set validate(ExecutionContext executionContext, JsonNo // get the Validator state object storing validation data ValidatorState state = executionContext.getValidatorState(); - Set childSchemaErrors = new LinkedHashSet<>(); + SetView childSchemaErrors = null; for (JsonSchema schema : this.schemas) { Set localErrors = null; @@ -58,8 +59,13 @@ public Set validate(ExecutionContext executionContext, JsonNo } else { localErrors = schema.walk(executionContext, node, rootNode, instanceLocation, true); } - - childSchemaErrors.addAll(localErrors); + + if (localErrors != null && !localErrors.isEmpty()) { + if (childSchemaErrors == null) { + childSchemaErrors = new SetView<>(); + } + childSchemaErrors.union(localErrors); + } if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { final Iterator arrayElements = this.schemaNode.elements(); @@ -91,7 +97,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } } - return Collections.unmodifiableSet(childSchemaErrors); + return childSchemaErrors != null ? childSchemaErrors : Collections.emptySet(); } @Override diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index ced603e7c..1a6e514fc 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -17,12 +17,12 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.utils.SetView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; -import java.util.stream.Collectors; /** * {@link JsonValidator} for anyOf. @@ -64,7 +64,7 @@ public Set validate(ExecutionContext executionContext, JsonNo boolean initialHasMatchedNode = state.hasMatchedNode(); - Set allErrors = new LinkedHashSet<>(); + SetView allErrors = null; int numberOfValidSubSchemas = 0; try { @@ -82,7 +82,10 @@ public Set validate(ExecutionContext executionContext, JsonNo // ignore it // For union type, it is a must to call TypeValidator if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) { - allErrors.addAll(typeValidator.validate(executionContext, node, rootNode, instanceLocation)); + if (allErrors == null) { + allErrors = new SetView<>(); + } + allErrors.union(typeValidator.validate(executionContext, node, rootNode, instanceLocation)); continue; } } @@ -105,63 +108,60 @@ public Set validate(ExecutionContext executionContext, JsonNo if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) && canShortCircuit() && canShortCircuit(executionContext)) { - // Clear all errors. - allErrors.clear(); + // Clear all errors. Note that this is checked in finally. + allErrors = null; // return empty errors. return errors; } else if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { if (this.discriminatorContext.isDiscriminatorMatchFound()) { if (!errors.isEmpty()) { - allErrors.addAll(errors); - allErrors.add(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()) - .failFast(executionContext.isFailFast()) - .arguments(DISCRIMINATOR_REMARK).build()); + // The following is to match the previous logic adding to all errors + // which is generally discarded as it returns errors but the allErrors + // is getting processed in finally + if (allErrors == null) { + allErrors = new SetView<>(); + } + allErrors.union(Collections + .singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.isFailFast()).arguments(DISCRIMINATOR_REMARK) + .build())); } else { - // Clear all errors. - allErrors.clear(); + // Clear all errors. Note that this is checked in finally. + allErrors = null; } return errors; } } - allErrors.addAll(errors); + if (allErrors == null) { + allErrors = new SetView<>(); + } + allErrors.union(errors); } } finally { // Restore flag executionContext.setFailFast(failFast); } - // determine only those errors which are NOT of type "required" property missing - Set childNotRequiredErrors = allErrors.stream() - .filter(error -> !ValidatorTypeCode.REQUIRED.getValue().equals(error.getType())) - .collect(Collectors.toCollection(LinkedHashSet::new)); - - // in case we had at least one (anyOf, i.e. any number >= 1 of) valid subschemas, we can remove all other errors about "required" properties - if (numberOfValidSubSchemas >= 1 && childNotRequiredErrors.isEmpty()) { - allErrors = childNotRequiredErrors; - } - if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && this.discriminatorContext.isActive()) { - final Set errors = new LinkedHashSet<>(); - errors.add(message().instanceNode(node).instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .arguments( "based on the provided discriminator. No alternative could be chosen based on the discriminator property") .build()); - return Collections.unmodifiableSet(errors); } } finally { if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { executionContext.leaveDiscriminatorContextImmediately(instanceLocation); } - if (allErrors.isEmpty()) { + if (allErrors == null || allErrors.isEmpty()) { state.setMatchedNode(true); } } if (numberOfValidSubSchemas >= 1) { return Collections.emptySet(); } - return Collections.unmodifiableSet(allErrors); + return allErrors != null ? allErrors : Collections.emptySet(); } @Override diff --git a/src/main/java/com/networknt/schema/DependenciesValidator.java b/src/main/java/com/networknt/schema/DependenciesValidator.java index 5ecaddf33..596e77aa3 100644 --- a/src/main/java/com/networknt/schema/DependenciesValidator.java +++ b/src/main/java/com/networknt/schema/DependenciesValidator.java @@ -49,7 +49,7 @@ public DependenciesValidator(SchemaLocation schemaLocation, JsonNodePath evaluat if (pvalue.isArray()) { List depsProps = propertyDeps.get(pname); if (depsProps == null) { - depsProps = new ArrayList(); + depsProps = new ArrayList<>(); propertyDeps.put(pname, depsProps); } for (int i = 0; i < pvalue.size(); i++) { @@ -65,7 +65,7 @@ public DependenciesValidator(SchemaLocation schemaLocation, JsonNodePath evaluat public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - Set errors = new LinkedHashSet(); + Set errors = null; for (Iterator it = node.fieldNames(); it.hasNext(); ) { String pname = it.next(); @@ -73,6 +73,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (deps != null && !deps.isEmpty()) { for (String field : deps) { if (node.get(field) == null) { + if (errors == null) { + errors = new LinkedHashSet<>(); + } errors.add(message().instanceNode(node).property(pname).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .failFast(executionContext.isFailFast()) @@ -82,11 +85,16 @@ public Set validate(ExecutionContext executionContext, JsonNo } JsonSchema schema = schemaDeps.get(pname); if (schema != null) { - errors.addAll(schema.validate(executionContext, node, rootNode, instanceLocation)); + Set schemaDepsErrors = schema.validate(executionContext, node, rootNode, instanceLocation); + if (!schemaDepsErrors.isEmpty()) { + if (errors == null) { + errors = new LinkedHashSet<>(); + } + errors.addAll(schemaDepsErrors); + } } } - - return Collections.unmodifiableSet(errors); + return errors == null || errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); } @Override diff --git a/src/main/java/com/networknt/schema/DependentSchemas.java b/src/main/java/com/networknt/schema/DependentSchemas.java index 5781eb54b..253ed04d0 100644 --- a/src/main/java/com/networknt/schema/DependentSchemas.java +++ b/src/main/java/com/networknt/schema/DependentSchemas.java @@ -47,17 +47,21 @@ public DependentSchemas(SchemaLocation schemaLocation, JsonNodePath evaluationPa public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - Set errors = new LinkedHashSet<>(); - + Set errors = null; for (Iterator it = node.fieldNames(); it.hasNext(); ) { String pname = it.next(); JsonSchema schema = this.schemaDependencies.get(pname); if (schema != null) { - errors.addAll(schema.validate(executionContext, node, rootNode, instanceLocation)); + Set schemaDependenciesErrors = schema.validate(executionContext, node, rootNode, instanceLocation); + if (!schemaDependenciesErrors.isEmpty()) { + if (errors == null) { + errors = new LinkedHashSet<>(); + } + errors.addAll(schemaDependenciesErrors); + } } } - - return Collections.unmodifiableSet(errors); + return errors == null || errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); } @Override diff --git a/src/main/java/com/networknt/schema/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java index 1edce7bdc..c3c340ad1 100644 --- a/src/main/java/com/networknt/schema/IfValidator.java +++ b/src/main/java/com/networknt/schema/IfValidator.java @@ -66,7 +66,6 @@ public IfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, J @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - Set errors = new LinkedHashSet<>(); boolean ifConditionPassed = false; @@ -81,11 +80,11 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (ifConditionPassed && this.thenSchema != null) { - errors.addAll(this.thenSchema.validate(executionContext, node, rootNode, instanceLocation)); + return this.thenSchema.validate(executionContext, node, rootNode, instanceLocation); } else if (!ifConditionPassed && this.elseSchema != null) { - errors.addAll(this.elseSchema.validate(executionContext, node, rootNode, instanceLocation)); + return this.elseSchema.validate(executionContext, node, rootNode, instanceLocation); } - return Collections.unmodifiableSet(errors); + return Collections.emptySet(); } @Override diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index a6f2e41b3..1905886f2 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.networknt.schema.annotation.JsonNodeAnnotation; +import com.networknt.schema.utils.SetView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -119,7 +120,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } boolean hasAdditionalItem = false; - Set errors = new LinkedHashSet<>(); + SetView errors = new SetView(); if (node.isArray()) { int i = 0; for (JsonNode n : node) { @@ -143,10 +144,10 @@ public Set validate(ExecutionContext executionContext, JsonNo .keyword("additionalItems").value(true).build()); } } - return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); + return errors.isEmpty() ? Collections.emptySet() : errors; } - private boolean doValidate(ExecutionContext executionContext, Set errors, int i, JsonNode node, + private boolean doValidate(ExecutionContext executionContext, SetView errors, int i, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { boolean isAdditionalItem = false; JsonNodePath path = instanceLocation.append(i); @@ -156,14 +157,14 @@ private boolean doValidate(ExecutionContext executionContext, Set results = this.schema.validate(executionContext, node, rootNode, path); if (!results.isEmpty()) { - errors.addAll(results); + errors.union(results); } } else if (this.tupleSchema != null) { if (i < this.tupleSchema.size()) { // validate against tuple schema Set results = this.tupleSchema.get(i).validate(executionContext, node, rootNode, path); if (!results.isEmpty()) { - errors.addAll(results); + errors.union(results); } } else { if ((this.additionalItems != null && this.additionalItems) || this.additionalSchema != null) { @@ -174,21 +175,21 @@ private boolean doValidate(ExecutionContext executionContext, Set results = this.additionalSchema.validate(executionContext, node, rootNode, path); if (!results.isEmpty()) { - errors.addAll(results); + errors.union(results); } } else if (this.additionalItems != null) { if (this.additionalItems) { // evaluatedItems.add(path); } else { // no additional item allowed, return error - errors.add(message().instanceNode(rootNode).instanceLocation(instanceLocation) + errors.union(Collections.singleton(message().instanceNode(rootNode).instanceLocation(instanceLocation) .type("additionalItems") .messageKey("additionalItems") .evaluationPath(this.additionalItemsEvaluationPath) .schemaLocation(this.additionalItemsSchemaLocation) .schemaNode(this.additionalItemsSchemaNode) .locale(executionContext.getExecutionConfig().getLocale()) - .failFast(executionContext.isFailFast()).arguments(i).build()); + .failFast(executionContext.isFailFast()).arguments(i).build())); } } } diff --git a/src/main/java/com/networknt/schema/ItemsValidator202012.java b/src/main/java/com/networknt/schema/ItemsValidator202012.java index 1c955be33..196d4741b 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator202012.java +++ b/src/main/java/com/networknt/schema/ItemsValidator202012.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.networknt.schema.annotation.JsonNodeAnnotation; +import com.networknt.schema.utils.SetView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,8 +68,7 @@ public Set validate(ExecutionContext executionContext, JsonNo // ignores non-arrays if (node.isArray()) { - Set errors = new LinkedHashSet<>(); -// Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); + SetView errors = null; boolean evaluated = false; for (int i = this.prefixCount; i < node.size(); ++i) { JsonNodePath path = instanceLocation.append(i); @@ -87,7 +87,10 @@ public Set validate(ExecutionContext executionContext, JsonNo if (results.isEmpty()) { // evaluatedItems.add(path); } else { - errors.addAll(results); + if (errors == null) { + errors = new SetView<>(); + } + errors.union(results); } evaluated = true; } @@ -100,7 +103,7 @@ public Set validate(ExecutionContext executionContext, JsonNo .keyword(getKeyword()).value(true).build()); } } - return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); + return errors == null || errors.isEmpty() ? Collections.emptySet() : errors; } else { return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 1171366ef..8a93146e1 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -22,6 +22,7 @@ import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.serialization.JsonMapperFactory; import com.networknt.schema.serialization.YamlMapperFactory; +import com.networknt.schema.utils.SetView; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -544,7 +545,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } SchemaValidatorsConfig config = this.validationContext.getConfig(); - Set errors = null; + SetView errors = null; // Set the walkEnabled and isValidationEnabled flag in internal validator state. setValidatorState(executionContext, false, true); @@ -558,9 +559,9 @@ public Set validate(ExecutionContext executionContext, JsonNo // Do nothing if valid } else { if (errors == null) { - errors = new LinkedHashSet<>(); + errors = new SetView<>(); } - errors.addAll(results); + errors.union(results); } } } diff --git a/src/main/java/com/networknt/schema/JsonSchemaVersion.java b/src/main/java/com/networknt/schema/JsonSchemaVersion.java index a43a1d724..f2c29a932 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaVersion.java +++ b/src/main/java/com/networknt/schema/JsonSchemaVersion.java @@ -4,7 +4,7 @@ import java.util.List; public abstract class JsonSchemaVersion { - public static final List BUILTIN_FORMATS = new ArrayList(JsonMetaSchema.COMMON_BUILTIN_FORMATS); + public static final List BUILTIN_FORMATS = new ArrayList<>(JsonMetaSchema.COMMON_BUILTIN_FORMATS); static { // add version specific formats here. //BUILTIN_FORMATS.add(pattern("phone", "^\\+(?:[0-9] ?){6,14}[0-9]$")); diff --git a/src/main/java/com/networknt/schema/NotAllowedValidator.java b/src/main/java/com/networknt/schema/NotAllowedValidator.java index b21ca3e60..3d9952b83 100644 --- a/src/main/java/com/networknt/schema/NotAllowedValidator.java +++ b/src/main/java/com/networknt/schema/NotAllowedValidator.java @@ -28,7 +28,7 @@ public class NotAllowedValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(NotAllowedValidator.class); - private List fieldNames = new ArrayList(); + private final List fieldNames = new ArrayList<>(); public NotAllowedValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.NOT_ALLOWED, validationContext); diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 34a38afb7..21a26d792 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -17,6 +17,7 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.utils.SetView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,7 +45,7 @@ public OneOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - Set errors = new LinkedHashSet<>(); + Set errors = null; debug(logger, node, rootNode, instanceLocation); @@ -54,7 +55,7 @@ public Set validate(ExecutionContext executionContext, JsonNo state.setComplexValidator(true); int numberOfValidSchema = 0; - Set childErrors = new LinkedHashSet<>(); + SetView childErrors = null; // Save flag as nested schema evaluation shouldn't trigger fail fast boolean failFast = executionContext.isFailFast(); @@ -88,8 +89,11 @@ public Set validate(ExecutionContext executionContext, JsonNo break; } - if (reportChildErrors(executionContext)) { - childErrors.addAll(schemaErrors); + if (!schemaErrors.isEmpty() && reportChildErrors(executionContext)) { + if (childErrors == null) { + childErrors = new SetView<>(); + } + childErrors.union(schemaErrors); } } } finally { @@ -104,19 +108,22 @@ public Set validate(ExecutionContext executionContext, JsonNo .locale(executionContext.getExecutionConfig().getLocale()) .failFast(executionContext.isFailFast()) .arguments(Integer.toString(numberOfValidSchema)).build(); - errors.add(message); - errors.addAll(childErrors); + if (childErrors != null) { + errors = new SetView().union(Collections.singleton(message)).union(childErrors); + } else { + errors = Collections.singleton(message); + } } // Make sure to signal parent handlers we matched - if (errors.isEmpty()) { + if (errors == null || errors.isEmpty()) { state.setMatchedNode(true); } // reset the ValidatorState object resetValidatorState(executionContext); - return Collections.unmodifiableSet(errors); + return errors != null ? errors : Collections.emptySet(); } /** diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java index 029088121..d798503f6 100644 --- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.networknt.schema.annotation.JsonNodeAnnotation; +import com.networknt.schema.utils.SetView; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,13 +59,16 @@ public Set validate(ExecutionContext executionContext, JsonNo debug(logger, node, rootNode, instanceLocation); // ignores non-arrays if (node.isArray()) { - Set errors = new LinkedHashSet<>(); + SetView errors = null; int count = Math.min(node.size(), this.tupleSchema.size()); for (int i = 0; i < count; ++i) { JsonNodePath path = instanceLocation.append(i); Set results = this.tupleSchema.get(i).validate(executionContext, node.get(i), rootNode, path); if (!results.isEmpty()) { - errors.addAll(results); + if (errors == null) { + errors = new SetView<>(); + } + errors.union(results); } } @@ -86,7 +91,7 @@ public Set validate(ExecutionContext executionContext, JsonNo .keyword(getKeyword()).value(true).build()); } } - return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); + return errors == null || errors.isEmpty() ? Collections.emptySet() : errors; } else { return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index bc526a32f..2eb23b271 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.annotation.JsonNodeAnnotation; +import com.networknt.schema.utils.SetView; import com.networknt.schema.walk.WalkListenerRunner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,7 +50,7 @@ public PropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluatio public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - Set errors = null; + SetView errors = null; // get the Validator state object storing validation data ValidatorState state = executionContext.getValidatorState(); @@ -82,14 +83,14 @@ public Set validate(ExecutionContext executionContext, JsonNo Set result = propertySchema.validate(executionContext, propertyNode, rootNode, path); if (!result.isEmpty()) { if (errors == null) { - errors = new LinkedHashSet<>(); + errors = new SetView<>(); } - errors.addAll(result); + errors.union(result); } } else { // check if walker is enabled. If it is enabled it is upto the walker implementation to decide about the validation. if (errors == null) { - errors = new LinkedHashSet<>(); + errors = new SetView<>(); } walkSchema(executionContext, entry, node, rootNode, instanceLocation, state.isValidationEnabled(), errors, this.validationContext.getConfig().getPropertyWalkListenerRunner()); } @@ -102,24 +103,24 @@ public Set validate(ExecutionContext executionContext, JsonNo } } else { // check whether the node which has not matched was mandatory or not - if (getParentSchema().hasRequiredValidator()) { - - // The required validator runs for all properties in the node and not just the - // current propertyNode - if (requiredErrors == null) { - requiredErrors = getParentSchema().getRequiredValidator().validate(executionContext, node, rootNode, instanceLocation); + // the node was mandatory, decide which behavior to employ when validator has + // not matched + if (state.isComplexValidator()) { + if (getParentSchema().hasRequiredValidator()) { + + // The required validator runs for all properties in the node and not just the + // current propertyNode + if (requiredErrors == null) { + // Note that the results of the required validator shouldn't be added to the errors here, the required validator + // will still trigger normally in the schema + requiredErrors = getParentSchema().getRequiredValidator().validate(executionContext, node, + rootNode, instanceLocation); + } if (!requiredErrors.isEmpty()) { - // the node was mandatory, decide which behavior to employ when validator has not matched - if (state.isComplexValidator()) { - // this was a complex validator (ex oneOf) and the node has not been matched - state.setMatchedNode(false); - return Collections.emptySet(); - } - if (errors == null) { - errors = new LinkedHashSet<>(); - } - errors.addAll(requiredErrors); + // this was a complex validator (ex oneOf) and the node has not been matched + state.setMatchedNode(false); + return Collections.emptySet(); } } } @@ -134,18 +135,18 @@ public Set validate(ExecutionContext executionContext, JsonNo .build()); } - return errors == null || errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); + return errors == null || errors.isEmpty() ? Collections.emptySet() : errors; } @Override public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { - HashSet validationMessages = new LinkedHashSet<>(); + SetView validationMessages = new SetView<>(); if (this.validationContext.getConfig().getApplyDefaultsStrategy().shouldApplyPropertyDefaults() && null != node && node.getNodeType() == JsonNodeType.OBJECT) { applyPropertyDefaults((ObjectNode) node); } if (shouldValidateSchema) { - validationMessages.addAll(validate(executionContext, node, rootNode, instanceLocation)); + validationMessages.union(validate(executionContext, node, rootNode, instanceLocation)); } else { WalkListenerRunner propertyWalkListenerRunner = this.validationContext.getConfig().getPropertyWalkListenerRunner(); for (Map.Entry entry : this.schemas.entrySet()) { @@ -189,7 +190,7 @@ private static JsonNode getDefaultNode(final Map.Entry entry private void walkSchema(ExecutionContext executionContext, Map.Entry entry, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema, - Set validationMessages, WalkListenerRunner propertyWalkListenerRunner) { + SetView validationMessages, WalkListenerRunner propertyWalkListenerRunner) { JsonSchema propertySchema = entry.getValue(); JsonNode propertyNode = (node == null ? null : node.get(entry.getKey())); JsonNodePath path = instanceLocation.append(entry.getKey()); @@ -198,7 +199,7 @@ private void walkSchema(ExecutionContext executionContext, Map.Entry validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - Set errors = new LinkedHashSet(); + Set errors = null; for (Iterator it = node.fieldNames(); it.hasNext(); ) { final String pname = it.next(); final TextNode pnameText = TextNode.valueOf(pname); @@ -45,16 +45,19 @@ public Set validate(ExecutionContext executionContext, JsonNo for (final ValidationMessage schemaError : schemaErrors) { final String path = schemaError.getInstanceLocation().toString(); String msg = schemaError.getMessage(); - if (msg.startsWith(path)) + if (msg.startsWith(path)) { msg = msg.substring(path.length()).replaceFirst("^:\\s*", ""); - + } + if (errors == null) { + errors = new LinkedHashSet<>(); + } errors.add( message().property(pname).instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .failFast(executionContext.isFailFast()).arguments(pname, msg).build()); } } - return Collections.unmodifiableSet(errors); + return errors == null || errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); } diff --git a/src/main/java/com/networknt/schema/RequiredValidator.java b/src/main/java/com/networknt/schema/RequiredValidator.java index e9a19cc14..1b6b816d0 100644 --- a/src/main/java/com/networknt/schema/RequiredValidator.java +++ b/src/main/java/com/networknt/schema/RequiredValidator.java @@ -28,7 +28,7 @@ public class RequiredValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class); - private List fieldNames = new ArrayList(); + private final List fieldNames = new ArrayList<>(); public RequiredValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.REQUIRED, validationContext); diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index 4d7543b8f..c051ea3fb 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -31,7 +31,7 @@ public class UnionTypeValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnionTypeValidator.class); - private final List schemas = new ArrayList(); + private final List schemas = new ArrayList<>(); private final String error; public UnionTypeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { diff --git a/src/main/java/com/networknt/schema/utils/SetView.java b/src/main/java/com/networknt/schema/utils/SetView.java new file mode 100644 index 000000000..08749070e --- /dev/null +++ b/src/main/java/com/networknt/schema/utils/SetView.java @@ -0,0 +1,204 @@ +package com.networknt.schema.utils; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * View of a list of sets. + *

+ * This is used for performance to reduce copies but breaks the semantics of the + * set where the elements must all be unique. + * + * @param the type contains in the set + */ +public class SetView implements Set { + private final List> sets = new ArrayList<>(); + + /** + * Adds a set to the view. + * + * @param set to add to the view + * @return the view + */ + public SetView union(Set set) { + if (set != null && !set.isEmpty()) { + this.sets.add(set); + } + return this; + } + + @Override + public int size() { + int size = 0; + for (Set set : sets) { + size += set.size(); + } + return size; + } + + @Override + public boolean isEmpty() { + return sets.isEmpty(); + } + + @Override + public boolean contains(Object o) { + for (Set set : sets) { + if (set.contains(o)) { + return true; + } + } + return false; + } + + @Override + public Iterator iterator() { + return new SetViewIterator(this); + } + + @Override + public Object[] toArray() { + int size = size(); + Object[] result = new Object[size]; + Iterator iterator = iterator(); + for (int x = 0; x < size; x++) { + result[x] = iterator.hasNext() ? iterator.next() : null; + } + return result; + } + + @SuppressWarnings("unchecked") + @Override + public T[] toArray(T[] a) { + int size = size(); + T[] result = size <= a.length ? a : (T[]) Array.newInstance(a.getClass().getComponentType(), size); + Iterator iterator = iterator(); + for (int x = 0; x < size; x++) { + result[x] = iterator.hasNext() ? (T) iterator.next() : null; + } + return result; + } + + @Override + public boolean containsAll(Collection c) { + for (Object e : c) { + if (!contains(e)) { + return false; + } + } + return true; + } + + @Override + public boolean add(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection coll) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection coll) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection coll) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + /** + * Iterator. + * + * @param the type contains in the set + */ + public static class SetViewIterator implements Iterator { + private Iterator> sets = null; + private Iterator current = null; + + public SetViewIterator(SetView view) { + this.sets = view.sets.iterator(); + if (this.sets.hasNext()) { + this.current = this.sets.next().iterator(); + } + } + + @Override + public boolean hasNext() { + if (this.current.hasNext()) { + return true; + } + while (this.sets.hasNext()) { + this.current = this.sets.next().iterator(); + if (this.current.hasNext()) { + return true; + } + } + return false; + } + + @Override + public E next() { + return this.current.next(); + } + } + + @Override + public int hashCode() { + return Objects.hash(sets); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Set)) { + return false; + } + Collection collection = (Collection) obj; + if (collection.size() != size()) { + return false; + } + try { + return containsAll(collection); + } catch (ClassCastException ignore) { + return false; + } catch (NullPointerException ignore) { + return false; + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append('['); + Iterator iterator = iterator(); + if (iterator.hasNext()) { + builder.append(iterator.next().toString()); + } + while (iterator.hasNext()) { + builder.append(", "); + builder.append(iterator.next().toString()); + } + builder.append(']'); + return builder.toString(); + } +} diff --git a/src/test/java/com/networknt/schema/utils/SetViewTest.java b/src/test/java/com/networknt/schema/utils/SetViewTest.java new file mode 100644 index 000000000..7b039cc85 --- /dev/null +++ b/src/test/java/com/networknt/schema/utils/SetViewTest.java @@ -0,0 +1,209 @@ +/* + * 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.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +/** + * Test for SetView. + */ +class SetViewTest { + + @Test + void testUnion() { + Set a = new LinkedHashSet<>(); + Set b = new LinkedHashSet<>(); + Set c = new LinkedHashSet<>(); + a.add(1); + a.add(2); + c.add(3); + + Set view = new SetView().union(a).union(b).union(c); + assertEquals(3, view.size()); + List values = view.stream().collect(Collectors.toList()); + assertEquals(1, values.get(0)); + assertEquals(2, values.get(1)); + assertEquals(3, values.get(2)); + } + + @Test + void testToString() { + Set a = new LinkedHashSet<>(); + Set b = new LinkedHashSet<>(); + Set c = new LinkedHashSet<>(); + a.add(1); + a.add(2); + c.add(3); + + Set view = new SetView().union(a).union(b).union(c); + String value = view.toString(); + assertEquals("[1, 2, 3]", value); + } + + @Test + void testIsEmpty() { + Set a = new LinkedHashSet<>(); + a.add(1); + a.add(2); + + SetView view = new SetView<>(); + assertTrue(view.isEmpty()); + view.union(a); + assertFalse(view.isEmpty()); + } + + @Test + void testEquals() { + Set a = new LinkedHashSet<>(); + Set b = new LinkedHashSet<>(); + Set c = new LinkedHashSet<>(); + a.add(1); + a.add(2); + c.add(3); + + Set view = new SetView().union(a).union(b).union(c); + assertEquals(3, view.size()); + + Set result = new HashSet<>(); + result.add(1); + result.add(2); + result.add(3); + assertEquals(result, view); + } + + @Test + void testContains() { + Set a = new LinkedHashSet<>(); + Set b = new LinkedHashSet<>(); + Set c = new LinkedHashSet<>(); + a.add(1); + a.add(2); + c.add(3); + + Set view = new SetView().union(a).union(b).union(c); + assertTrue(view.contains(1)); + assertTrue(view.contains(2)); + assertTrue(view.contains(3)); + assertFalse(view.contains(4)); + } + + @Test + void testContainsAll() { + Set a = new LinkedHashSet<>(); + Set b = new LinkedHashSet<>(); + Set c = new LinkedHashSet<>(); + a.add(1); + a.add(2); + c.add(3); + + Set view = new SetView().union(a).union(b).union(c); + Set result = new HashSet<>(); + result.add(1); + result.add(2); + result.add(3); + assertTrue(view.containsAll(result)); + result.add(4); + assertFalse(view.containsAll(result)); + } + + @Test + void testToArray() { + Set a = new LinkedHashSet<>(); + Set b = new LinkedHashSet<>(); + Set c = new LinkedHashSet<>(); + a.add(1); + a.add(2); + c.add(3); + + Set view = new SetView().union(a).union(b).union(c); + assertEquals(3, view.size()); + + Object[] result = view.toArray(); + assertEquals(3, result.length); + assertEquals(1, result[0]); + assertEquals(2, result[1]); + assertEquals(3, result[2]); + } + + @Test + void testToArrayArray() { + Set a = new LinkedHashSet<>(); + Set b = new LinkedHashSet<>(); + Set c = new LinkedHashSet<>(); + a.add(1); + a.add(2); + c.add(3); + + Set view = new SetView().union(a).union(b).union(c); + assertEquals(3, view.size()); + + Integer[] result = view.toArray(new Integer[0]); + assertEquals(3, result.length); + assertEquals(1, result[0]); + assertEquals(2, result[1]); + assertEquals(3, result[2]); + } + + @Test + void testAddAll() { + Set view = new SetView<>(); + assertThrows(UnsupportedOperationException.class, () -> view.addAll(Collections.singleton(1))); + } + + @Test + void testAdd() { + Set view = new SetView<>(); + assertThrows(UnsupportedOperationException.class, () -> view.add(1)); + } + + @Test + void testClear() { + Set view = new SetView<>(); + assertThrows(UnsupportedOperationException.class, () -> view.clear()); + } + + @Test + void testRemove() { + Set view = new SetView<>(); + assertThrows(UnsupportedOperationException.class, () -> view.remove(1)); + } + + @Test + void testRemoveAll() { + Set view = new SetView<>(); + assertThrows(UnsupportedOperationException.class, () -> view.removeAll(Collections.singleton(1))); + } + + @Test + void testRetainAll() { + Set view = new SetView<>(); + assertThrows(UnsupportedOperationException.class, () -> view.retainAll(Collections.singleton(1))); + } + +}