From 033c5b73e4b4790507f197e9e156dc563895bba2 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 12 Jan 2024 08:58:30 +0800 Subject: [PATCH 01/65] Fix ref --- src/main/java/com/networknt/schema/JsonSchema.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index bdf2d4763..4f00147b2 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -81,7 +81,8 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc .putIfAbsent(this.currentUri != null ? this.currentUri.toString() : this.id, this); } if (this.anchor != null) { - this.validationContext.getSchemaResources().putIfAbsent(this.currentUri.toString() + "#" + anchor, this); + this.validationContext.getSchemaResources() + .putIfAbsent(this.schemaLocation.getAbsoluteIri().toString() + "#" + anchor, this); } getValidators(); } From a5a4e9fbb031a49b86a164609c66af9cc018b9bd Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:34:46 +0800 Subject: [PATCH 02/65] Refactor --- .../networknt/schema/BaseJsonValidator.java | 9 +- .../java/com/networknt/schema/JsonSchema.java | 79 +---- .../networknt/schema/JsonSchemaFactory.java | 269 +++++------------- .../com/networknt/schema/RefValidator.java | 58 +--- .../com/networknt/schema/SchemaLocation.java | 3 + .../schema/SchemaValidatorsConfig.java | 49 +--- .../networknt/schema/ValidationContext.java | 37 +-- ...URIFetcher.java => AbsoluteIriMapper.java} | 54 ++-- .../schema/uri/ClasspathSchemaLoader.java | 48 ++++ .../schema/uri/ClasspathURLFactory.java | 63 ---- .../schema/uri/ClasspathURLFetcher.java | 41 --- .../schema/uri/ClasspathURLStreamHandler.java | 102 ------- .../schema/uri/DefaultSchemaLoader.java | 55 ++++ .../schema/uri/InputStreamSource.java | 33 +++ .../schema/uri/MapAbsoluteIriMapper.java | 26 ++ .../schema/uri/PrefixAbsoluteIriMapper.java | 25 ++ .../networknt/schema/uri/SchemaLoader.java | 26 ++ .../com/networknt/schema/uri/URIFactory.java | 39 --- .../schema/uri/URISchemeFactory.java | 102 ------- .../schema/uri/URISchemeFetcher.java | 52 ---- .../networknt/schema/uri/URITranslator.java | 161 ----------- .../com/networknt/schema/uri/URLFactory.java | 64 ----- .../com/networknt/schema/uri/URLFetcher.java | 90 ------ .../networknt/schema/uri/URNURIFactory.java | 30 -- .../networknt/schema/uri/UriSchemaLoader.java | 31 ++ .../com/networknt/schema/walk/WalkEvent.java | 6 +- .../schema/AbstractJsonSchemaTestSuite.java | 14 +- .../schema/BaseJsonSchemaValidatorTest.java | 3 +- .../com/networknt/schema/CustomUriTest.java | 31 +- .../schema/CyclicDependencyTest.java | 5 +- .../com/networknt/schema/Issue285Test.java | 16 +- .../com/networknt/schema/Issue314Test.java | 1 - .../schema/Issue366FailFastTest.java | 7 +- .../schema/Issue366FailSlowTest.java | 7 +- .../com/networknt/schema/Issue428Test.java | 3 +- .../com/networknt/schema/Issue461Test.java | 5 +- .../com/networknt/schema/Issue518Test.java | 2 - .../com/networknt/schema/Issue619Test.java | 14 +- .../com/networknt/schema/Issue665Test.java | 12 +- .../com/networknt/schema/Issue824Test.java | 13 +- .../com/networknt/schema/Issue928Test.java | 12 +- .../schema/JsonSchemaFactoryUriCacheTest.java | 25 +- .../schema/OpenAPI30JsonSchemaTest.java | 3 +- ...ursiveReferenceValidatorExceptionTest.java | 3 +- .../networknt/schema/SharedConfigTest.java | 2 +- .../com/networknt/schema/UriMappingTest.java | 87 ++---- .../java/com/networknt/schema/UrnTest.java | 26 +- 47 files changed, 477 insertions(+), 1366 deletions(-) rename src/main/java/com/networknt/schema/uri/{URIFetcher.java => AbsoluteIriMapper.java} (66%) create mode 100644 src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java delete mode 100644 src/main/java/com/networknt/schema/uri/ClasspathURLFactory.java delete mode 100644 src/main/java/com/networknt/schema/uri/ClasspathURLFetcher.java delete mode 100644 src/main/java/com/networknt/schema/uri/ClasspathURLStreamHandler.java create mode 100644 src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java create mode 100644 src/main/java/com/networknt/schema/uri/InputStreamSource.java create mode 100644 src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java create mode 100644 src/main/java/com/networknt/schema/uri/PrefixAbsoluteIriMapper.java create mode 100644 src/main/java/com/networknt/schema/uri/SchemaLoader.java delete mode 100644 src/main/java/com/networknt/schema/uri/URIFactory.java delete mode 100644 src/main/java/com/networknt/schema/uri/URISchemeFactory.java delete mode 100644 src/main/java/com/networknt/schema/uri/URISchemeFetcher.java delete mode 100644 src/main/java/com/networknt/schema/uri/URITranslator.java delete mode 100644 src/main/java/com/networknt/schema/uri/URLFactory.java delete mode 100644 src/main/java/com/networknt/schema/uri/URLFetcher.java delete mode 100644 src/main/java/com/networknt/schema/uri/URNURIFactory.java create mode 100644 src/main/java/com/networknt/schema/uri/UriSchemaLoader.java diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 98596cbcb..724e03d7d 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -98,14 +98,9 @@ private static JsonSchema obtainSubSchemaNode(final JsonNode schemaNode, final V return null; } - final URI uri; - try { - uri = validationContext.getURIFactory().create(node.textValue()); - } catch (IllegalArgumentException e) { - return null; - } + final SchemaLocation schemaLocation = SchemaLocation.of(node.textValue()); - return validationContext.getJsonSchemaFactory().getSchema(uri, validationContext.getConfig()); + return validationContext.getJsonSchemaFactory().getSchema(schemaLocation, validationContext.getConfig()); } protected static boolean equals(double n1, double n2) { diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 4f00147b2..fa05efcf6 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -44,16 +44,6 @@ public class JsonSchema extends BaseJsonValidator { private boolean validatorsLoaded = false; private boolean dynamicAnchor = false; - /** - * This is the current uri of this schema. This uri could refer to the uri of this schema's file - * or it could potentially be a uri that has been altered by an id. An 'id' is able to completely overwrite - * the current uri or add onto it. This is necessary so that '$ref's are able to be relative to a - * combination of the current schema file's uri and 'id' uris visible to this schema. - *

- * This can be null. If it is null, then the creation of relative uris will fail. However, an absolute - * 'id' would still be able to specify an absolute uri. - */ - private URI currentUri; private JsonValidator requiredValidator = null; private TypeValidator typeValidator; @@ -62,23 +52,22 @@ public class JsonSchema extends BaseJsonValidator { private final String id; private final String anchor; - static JsonSchema from(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { - return new JsonSchema(validationContext, schemaLocation, evaluationPath, currentUri, schemaNode, parent, suppressSubSchemaRetrieval); + static JsonSchema from(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { + return new JsonSchema(validationContext, schemaLocation, evaluationPath, schemaNode, parent, suppressSubSchemaRetrieval); } - private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, - JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { - super(schemaLocation, evaluationPath, schemaNode, parent, null, null, validationContext, - suppressSubSchemaRetrieval); + private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, + JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { + super(schemaLocation.resolve(validationContext.resolveSchemaId(schemaNode)), evaluationPath, schemaNode, parent, + null, null, validationContext, suppressSubSchemaRetrieval); this.validationContext = validationContext; this.metaSchema = validationContext.getMetaSchema(); - this.currentUri = combineCurrentUriWithIds(currentUri, schemaNode); initializeConfig(); this.id = validationContext.resolveSchemaId(this.schemaNode); this.anchor = validationContext.getMetaSchema().readAnchor(this.schemaNode); if (this.id != null) { this.validationContext.getSchemaResources() - .putIfAbsent(this.currentUri != null ? this.currentUri.toString() : this.id, this); + .putIfAbsent(this.schemaLocation != null ? this.schemaLocation.toString() : this.id, this); } if (this.anchor != null) { this.validationContext.getSchemaResources() @@ -105,7 +94,6 @@ protected JsonSchema(JsonSchema copy) { this.metaSchema = copy.metaSchema; this.validatorsLoaded = copy.validatorsLoaded; this.dynamicAnchor = copy.dynamicAnchor; - this.currentUri = copy.currentUri; this.requiredValidator = copy.requiredValidator; this.typeValidator = copy.typeValidator; this.keywordWalkListenerRunner = copy.keywordWalkListenerRunner; @@ -126,8 +114,7 @@ protected JsonSchema(JsonSchema copy) { */ public JsonSchema fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath refEvaluationPath) { JsonSchema copy = new JsonSchema(this); - copy.validationContext = new ValidationContext(copy.validationContext.getURIFactory(), - copy.getValidationContext().getURNFactory(), copy.getValidationContext().getMetaSchema(), + copy.validationContext = new ValidationContext(copy.getValidationContext().getMetaSchema(), copy.getValidationContext().getJsonSchemaFactory(), refEvaluationParentSchema.validationContext.getConfig(), copy.getValidationContext().getSchemaReferences(), copy.getValidationContext().getSchemaResources()); @@ -145,8 +132,7 @@ public JsonSchema fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath ref public JsonSchema withConfig(SchemaValidatorsConfig config) { if (!this.getValidationContext().getConfig().equals(config)) { JsonSchema copy = new JsonSchema(this); - copy.validationContext = new ValidationContext(copy.validationContext.getURIFactory(), - copy.getValidationContext().getURNFactory(), copy.getValidationContext().getMetaSchema(), + copy.validationContext = new ValidationContext(copy.getValidationContext().getMetaSchema(), copy.getValidationContext().getJsonSchemaFactory(), config, copy.getValidationContext().getSchemaReferences(), copy.getValidationContext().getSchemaResources()); @@ -164,37 +150,6 @@ ValidationContext getValidationContext() { return this.validationContext; } - private URI combineCurrentUriWithIds(URI currentUri, JsonNode schemaNode) { - final String id = this.validationContext.resolveSchemaId(schemaNode); - if (id == null) { - return currentUri; - } else if (isUriFragmentWithNoContext(currentUri, id)) { - return null; - } else { - try { - return this.validationContext.getURIFactory().create(currentUri, id); - } catch (IllegalArgumentException e) { - SchemaLocation path = schemaLocation.append(this.metaSchema.getIdKeyword()); - ValidationMessage validationMessage = ValidationMessage.builder().code(ValidatorTypeCode.ID.getValue()) - .type(ValidatorTypeCode.ID.getValue()).instanceLocation(path.getFragment()) - .evaluationPath(path.getFragment()) - .arguments(currentUri == null ? "null" : currentUri.toString(), id) - .messageFormatter(args -> this.validationContext.getConfig().getMessageSource().getMessage( - ValidatorTypeCode.ID.getValue(), this.validationContext.getConfig().getLocale(), args)) - .build(); - throw new JsonSchemaException(validationMessage); - } - } - } - - private static boolean isUriFragmentWithNoContext(URI currentUri, String id) { - return id.startsWith("#") && (currentUri == null || currentUri.toString().startsWith("#")); - } - - public URI getCurrentUri() { - return this.currentUri; - } - /** * Find the schema node for $ref attribute. * @@ -274,23 +229,9 @@ public boolean isSchemaResourceRoot() { return true; } // The schema should not cross - if (getCurrentUri() != null && getParentSchema().getCurrentUri() == null) { - return true; - } - if (getCurrentUri() == null && getParentSchema().getCurrentUri() != null) { + if (!getSchemaLocation().getAbsoluteIri().equals(getParentSchema().getSchemaLocation().getAbsoluteIri())) { return true; } - if (getCurrentUri() != null && getParentSchema().getCurrentUri() != null) { - if (!Objects.equals(getCurrentUri().getScheme(), getParentSchema().getCurrentUri().getScheme())) { - return true; - } - if (!Objects.equals(getCurrentUri().getHost(), getParentSchema().getCurrentUri().getHost())) { - return true; - } - if (!Objects.equals(getCurrentUri().getPath(), getParentSchema().getCurrentUri().getPath())) { - return true; - } - } return false; } diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 12453f4ac..67b795c22 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -20,8 +20,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.networknt.schema.uri.*; -import com.networknt.schema.uri.URITranslator.CompositeURITranslator; -import com.networknt.schema.urn.URNFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,12 +27,13 @@ import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; public class JsonSchemaFactory { private static final Logger logger = LoggerFactory @@ -45,36 +44,14 @@ public static class Builder { private ObjectMapper objectMapper = null; private YAMLMapper yamlMapper = null; private String defaultMetaSchemaURI; - private final Map uriFactoryMap = new HashMap(); - private final Map uriFetcherMap = new HashMap(); - private URNFactory urnFactory; private final ConcurrentMap jsonMetaSchemas = new ConcurrentHashMap(); - private final Map uriMap = new HashMap(); + private List schemaLoaders = new ArrayList<>(); + private List absoluteIriMappers = new ArrayList<>(); private boolean enableUriSchemaCache = true; - private final CompositeURITranslator uriTranslators = new CompositeURITranslator(); public Builder() { - // Adds support for creating {@link URL}s. - final URIFactory urlFactory = new URLFactory(); - for (final String scheme : URLFactory.SUPPORTED_SCHEMES) { - this.uriFactoryMap.put(scheme, urlFactory); - } - // Adds support for creating URNs. - this.uriFactoryMap.put(URNURIFactory.SCHEME, new URNURIFactory()); - - // Adds support for fetching with {@link URL}s. - final URIFetcher urlFetcher = new URLFetcher(); - for (final String scheme : URLFetcher.SUPPORTED_SCHEMES) { - this.uriFetcherMap.put(scheme, urlFetcher); - } - - // Adds support for creating and fetching with classpath {@link URL}s. - final URIFactory classpathURLFactory = new ClasspathURLFactory(); - final URIFetcher classpathURLFetcher = new ClasspathURLFetcher(); - for (final String scheme : ClasspathURLFactory.SUPPORTED_SCHEMES) { - this.uriFactoryMap.put(scheme, classpathURLFactory); - this.uriFetcherMap.put(scheme, classpathURLFetcher); - } + this.schemaLoaders.add(new ClasspathSchemaLoader()); + this.schemaLoaders.add(new UriSchemaLoader()); } public Builder objectMapper(final ObjectMapper objectMapper) { @@ -92,42 +69,6 @@ public Builder defaultMetaSchemaURI(final String defaultMetaSchemaURI) { return this; } - /** - * Maps a number of schemes to a {@link URIFactory}. - * - * @param uriFactory the uri factory that will be used for the given schemes. - * @param schemes the scheme that the uri factory will be assocaited with. - * @return this builder. - */ - public Builder uriFactory(final URIFactory uriFactory, final String... schemes) { - return uriFactory(uriFactory, Arrays.asList(schemes)); - } - - public Builder uriFactory(final URIFactory uriFactory, final Iterable schemes) { - for (final String scheme : schemes) { - this.uriFactoryMap.put(scheme, uriFactory); - } - return this; - } - - /** - * Maps a number of schemes to a {@link URIFetcher}. - * - * @param uriFetcher the uri fetcher that will be used for the given schemes. - * @param schemes the scheme that the uri fetcher will be assocaited with. - * @return this builder. - */ - public Builder uriFetcher(final URIFetcher uriFetcher, final String... schemes) { - return uriFetcher(uriFetcher, Arrays.asList(schemes)); - } - - public Builder uriFetcher(final URIFetcher uriFetcher, final Iterable schemes) { - for (final String scheme : schemes) { - this.uriFetcherMap.put(scheme, uriFetcher); - } - return this; - } - public Builder addMetaSchema(final JsonMetaSchema jsonMetaSchema) { this.jsonMetaSchemas.put(normalizeMetaSchemaUri(jsonMetaSchema.getUri()) , jsonMetaSchema); return this; @@ -140,49 +81,18 @@ public Builder addMetaSchemas(final Collection jsonMet return this; } - /** - * @deprecated Use {@code addUriTranslator} instead. - * @param map the map of uri mappings. - * @return this builder. - */ - @Deprecated - public Builder addUriMappings(final Map map) { - this.uriMap.putAll(map); - return this; - } - - public Builder addUriTranslator(URITranslator translator) { - if (null != translator) { - this.uriTranslators.add(translator); - } - return this; - } - - public Builder addUrnFactory(URNFactory urnFactory) { - this.urnFactory = urnFactory; - return this; - } - - /** - * @deprecated No longer necessary. - * @param forceHttps ignored. - * @return this builder. - */ - public Builder forceHttps(boolean forceHttps) { + public Builder enableUriSchemaCache(boolean enableUriSchemaCache) { + this.enableUriSchemaCache = enableUriSchemaCache; return this; } - - /** - * @deprecated No longer necessary. - * @param removeEmptyFragmentSuffix ignored. - * @return this builder. - */ - public Builder removeEmptyFragmentSuffix(boolean removeEmptyFragmentSuffix) { + + public Builder schemaLoaders(Consumer> schemaLoaderCustomizer) { + schemaLoaderCustomizer.accept(this.schemaLoaders); return this; } - - public Builder enableUriSchemaCache(boolean enableUriSchemaCache) { - this.enableUriSchemaCache = enableUriSchemaCache; + + public Builder absoluteIriMappers(Consumer> absoluteIriCustomizer) { + absoluteIriCustomizer.accept(this.absoluteIriMappers); return this; } @@ -192,13 +102,10 @@ public JsonSchemaFactory build() { objectMapper == null ? new ObjectMapper() : objectMapper, yamlMapper == null ? new YAMLMapper(): yamlMapper, defaultMetaSchemaURI, - new URISchemeFactory(uriFactoryMap), - new URISchemeFetcher(uriFetcherMap), - urnFactory, + schemaLoaders, + absoluteIriMappers, jsonMetaSchemas, - uriMap, - enableUriSchemaCache, - uriTranslators + enableUriSchemaCache ); } } @@ -206,13 +113,11 @@ public JsonSchemaFactory build() { private final ObjectMapper jsonMapper; private final YAMLMapper yamlMapper; private final String defaultMetaSchemaURI; - private final URISchemeFactory uriFactory; - private final URISchemeFetcher uriFetcher; - private final CompositeURITranslator uriTranslators; - private final URNFactory urnFactory; + private final List schemaLoaders; + private final List absoluteIriMappers; + private final SchemaLoader schemaLoader; private final Map jsonMetaSchemas; - private final Map uriMap; - private final ConcurrentMap uriSchemaCache = new ConcurrentHashMap<>(); + private final ConcurrentMap uriSchemaCache = new ConcurrentHashMap<>(); private final boolean enableUriSchemaCache; @@ -220,42 +125,35 @@ private JsonSchemaFactory( final ObjectMapper jsonMapper, final YAMLMapper yamlMapper, final String defaultMetaSchemaURI, - final URISchemeFactory uriFactory, - final URISchemeFetcher uriFetcher, - final URNFactory urnFactory, + List schemaLoaders, + final List absoluteIriMappers, final Map jsonMetaSchemas, - final Map uriMap, - final boolean enableUriSchemaCache, - final CompositeURITranslator uriTranslators) { + final boolean enableUriSchemaCache) { if (jsonMapper == null) { throw new IllegalArgumentException("ObjectMapper must not be null"); } else if (yamlMapper == null) { throw new IllegalArgumentException("YAMLMapper must not be null"); } else if (defaultMetaSchemaURI == null || defaultMetaSchemaURI.trim().isEmpty()) { throw new IllegalArgumentException("defaultMetaSchemaURI must not be null or empty"); - } else if (uriFactory == null) { - throw new IllegalArgumentException("URIFactory must not be null"); - } else if (uriFetcher == null) { - throw new IllegalArgumentException("URIFetcher must not be null"); + } else if (schemaLoaders == null) { + throw new IllegalArgumentException("SchemaLoaders must not be null"); } else if (jsonMetaSchemas == null || jsonMetaSchemas.isEmpty()) { throw new IllegalArgumentException("Json Meta Schemas must not be null or empty"); } else if (jsonMetaSchemas.get(normalizeMetaSchemaUri(defaultMetaSchemaURI)) == null) { throw new IllegalArgumentException("Meta Schema for default Meta Schema URI must be provided"); - } else if (uriMap == null) { - throw new IllegalArgumentException("URL Mappings must not be null"); - } else if (uriTranslators == null) { - throw new IllegalArgumentException("URI Translators must not be null"); } this.jsonMapper = jsonMapper; this.yamlMapper = yamlMapper; this.defaultMetaSchemaURI = defaultMetaSchemaURI; - this.uriFactory = uriFactory; - this.uriFetcher = uriFetcher; - this.urnFactory = urnFactory; + this.schemaLoaders = schemaLoaders; + this.absoluteIriMappers = absoluteIriMappers; + this.schemaLoader = new DefaultSchemaLoader(schemaLoaders, absoluteIriMappers); this.jsonMetaSchemas = jsonMetaSchemas; - this.uriMap = uriMap; this.enableUriSchemaCache = enableUriSchemaCache; - this.uriTranslators = uriTranslators; + } + + public SchemaLoader getSchemaLoader() { + return this.schemaLoader; } /** @@ -309,39 +207,24 @@ public static Builder builder(final JsonSchemaFactory blueprint) { .addMetaSchemas(blueprint.jsonMetaSchemas.values()) .defaultMetaSchemaURI(blueprint.defaultMetaSchemaURI) .objectMapper(blueprint.jsonMapper) - .yamlMapper(blueprint.yamlMapper) - .addUriMappings(blueprint.uriMap); - - for (URITranslator translator: blueprint.uriTranslators) { - builder = builder.addUriTranslator(translator); - } - for (Map.Entry entry : blueprint.uriFactory.getURIFactories().entrySet()) { - builder = builder.uriFactory(entry.getValue(), entry.getKey()); - } - for (Map.Entry entry : blueprint.uriFetcher.getURIFetchers().entrySet()) { - builder = builder.uriFetcher(entry.getValue(), entry.getKey()); - } + .yamlMapper(blueprint.yamlMapper); + builder.schemaLoaders = blueprint.schemaLoaders; + builder.absoluteIriMappers = blueprint.absoluteIriMappers; return builder; } - protected JsonSchema newJsonSchema(final URI schemaUri, final JsonNode schemaNode, final SchemaValidatorsConfig config) { + protected JsonSchema newJsonSchema(final SchemaLocation schemaUri, final JsonNode schemaNode, final SchemaValidatorsConfig config) { final ValidationContext validationContext = createValidationContext(schemaNode, config); return doCreate(validationContext, getSchemaLocation(schemaUri, schemaNode, validationContext), - new JsonNodePath(validationContext.getConfig().getPathType()), schemaUri, schemaNode, null, false); - } - - public JsonSchema create(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, JsonNode schemaNode, JsonSchema parentSchema) { - return doCreate(validationContext, - null == schemaLocation ? getSchemaLocation(currentUri, schemaNode, validationContext) : schemaLocation, - evaluationPath, currentUri, schemaNode, parentSchema, false); + new JsonNodePath(validationContext.getConfig().getPathType()), schemaNode, null, false); } public JsonSchema create(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema) { - return create(validationContext, schemaLocation, evaluationPath, parentSchema.getCurrentUri(), schemaNode, parentSchema); + return doCreate(validationContext, schemaLocation, evaluationPath, schemaNode, parentSchema, false); } - private JsonSchema doCreate(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, JsonNode schemaNode, JsonSchema parentSchema, boolean suppressSubSchemaRetrieval) { - return JsonSchema.from(validationContext, schemaLocation, evaluationPath, currentUri, schemaNode, parentSchema, suppressSubSchemaRetrieval); + private JsonSchema doCreate(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, boolean suppressSubSchemaRetrieval) { + return JsonSchema.from(validationContext, schemaLocation, evaluationPath, schemaNode, parentSchema, suppressSubSchemaRetrieval); } /** @@ -352,7 +235,7 @@ private JsonSchema doCreate(ValidationContext validationContext, SchemaLocation * @param validationContext the validationContext * @return the schema location */ - protected SchemaLocation getSchemaLocation(URI schemaRetrievalUri, JsonNode schemaNode, + protected SchemaLocation getSchemaLocation(SchemaLocation schemaRetrievalUri, JsonNode schemaNode, ValidationContext validationContext) { String schemaLocation = validationContext.resolveSchemaId(schemaNode); if (schemaLocation == null && schemaRetrievalUri != null) { @@ -363,7 +246,7 @@ protected SchemaLocation getSchemaLocation(URI schemaRetrievalUri, JsonNode sche protected ValidationContext createValidationContext(final JsonNode schemaNode, SchemaValidatorsConfig config) { final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); - return new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, config); + return new ValidationContext(jsonMetaSchema, this, config); } private JsonMetaSchema findMetaSchemaForSchema(final JsonNode schemaNode) { @@ -384,17 +267,6 @@ private JsonMetaSchema fromId(String id) { .orElseThrow(() -> new JsonSchemaException("Unknown MetaSchema: " + id)); } - /** - * @return A shared {@link URI} factory that is used for creating the URI references in schemas. - */ - public URIFactory getUriFactory() { - return this.uriFactory; - } - - public URITranslator getUriTranslator() { - return this.uriTranslators.with(URITranslator.map(uriMap)); - } - public JsonSchema getSchema(final String schema, final SchemaValidatorsConfig config) { try { final JsonNode schemaNode = jsonMapper.readTree(schema); @@ -423,31 +295,23 @@ public JsonSchema getSchema(final InputStream schemaStream) { return getSchema(schemaStream, null); } - public JsonSchema getSchema(final URI schemaUri, final SchemaValidatorsConfig config) { - final URITranslator uriTranslator = null == config ? getUriTranslator() - : config.getUriTranslator().with(getUriTranslator()); - - final URI mappedUri; - try { - mappedUri = this.uriFactory.create(uriTranslator.translate(schemaUri).toString()); - } catch (IllegalArgumentException e) { - logger.error("Failed to create URI.", e); - throw new JsonSchemaException(e); - } - + public JsonSchema getSchema(final SchemaLocation schemaUri, final SchemaValidatorsConfig config) { if (enableUriSchemaCache) { - JsonSchema cachedUriSchema = uriSchemaCache.computeIfAbsent(mappedUri, key -> { - return getMappedSchema(schemaUri, config, mappedUri); + JsonSchema cachedUriSchema = uriSchemaCache.computeIfAbsent(schemaUri, key -> { + return getMappedSchema(schemaUri, config); }); return cachedUriSchema.withConfig(config); } - return getMappedSchema(schemaUri, config, mappedUri); + return getMappedSchema(schemaUri, config); } - protected JsonSchema getMappedSchema(final URI schemaUri, SchemaValidatorsConfig config, final URI mappedUri) { - try (InputStream inputStream = this.uriFetcher.fetch(mappedUri)) { + protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValidatorsConfig config) { + try (InputStream inputStream = this.schemaLoader.getSchema(schemaUri).getInputStream()) { + if (inputStream == null) { + throw new IOException("Cannot load schema uri"); + } final JsonNode schemaNode; - if (isYaml(mappedUri)) { + if (isYaml(schemaUri)) { schemaNode = yamlMapper.readTree(inputStream); } else { schemaNode = jsonMapper.readTree(inputStream); @@ -458,34 +322,33 @@ protected JsonSchema getMappedSchema(final URI schemaUri, SchemaValidatorsConfig JsonSchema jsonSchema; SchemaLocation schemaLocation = SchemaLocation.of(schemaUri.toString()); if (idMatchesSourceUri(jsonMetaSchema, schemaNode, schemaUri) || schemaUri.getFragment() == null - || "".equals(schemaUri.getFragment())) { - ValidationContext validationContext = new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, config); - jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, mappedUri, schemaNode, null, true /* retrieved via id, resolving will not change anything */); + || schemaUri.getFragment().getNameCount() == 0) { + ValidationContext validationContext = new ValidationContext(jsonMetaSchema, this, config); + jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, schemaNode, null, true /* retrieved via id, resolving will not change anything */); } else { // Subschema final ValidationContext validationContext = createValidationContext(schemaNode, config); - URI documentUri = "".equals(schemaUri.getSchemeSpecificPart()) ? new URI(schemaUri.getScheme(), schemaUri.getUserInfo(), schemaUri.getHost(), schemaUri.getPort(), schemaUri.getPath(), schemaUri.getQuery(), null) : new URI(schemaUri.getScheme(), schemaUri.getSchemeSpecificPart(), null); SchemaLocation documentLocation = new SchemaLocation(schemaLocation.getAbsoluteIri()); - JsonSchema document = doCreate(validationContext, documentLocation, evaluationPath, documentUri, schemaNode, null, false); + JsonSchema document = doCreate(validationContext, documentLocation, evaluationPath, schemaNode, null, false); JsonNode subSchemaNode = document.getRefSchemaNode("#" + schemaLocation.getFragment().toString()); if (subSchemaNode != null) { - jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, mappedUri, subSchemaNode, document, false); + jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, subSchemaNode, document, false); } else { throw new JsonSchemaException("Unable to find subschema"); } } return jsonSchema; - } catch (IOException | URISyntaxException e) { + } catch (IOException e) { logger.error("Failed to load json schema from {}", schemaUri, e); throw new JsonSchemaException(e); } } - public JsonSchema getSchema(final URI schemaUri) { + public JsonSchema getSchema(final SchemaLocation schemaUri) { return getSchema(schemaUri, new SchemaValidatorsConfig()); } - public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode, final SchemaValidatorsConfig config) { + public JsonSchema getSchema(final SchemaLocation schemaUri, final JsonNode jsonNode, final SchemaValidatorsConfig config) { return newJsonSchema(schemaUri, jsonNode, config); } @@ -494,7 +357,7 @@ public JsonSchema getSchema(final JsonNode jsonNode, final SchemaValidatorsConfi return newJsonSchema(null, jsonNode, config); } - public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode) { + public JsonSchema getSchema(final SchemaLocation schemaUri, final JsonNode jsonNode) { return newJsonSchema(schemaUri, jsonNode, null); } @@ -502,7 +365,7 @@ public JsonSchema getSchema(final JsonNode jsonNode) { return newJsonSchema(null, jsonNode, null); } - private boolean idMatchesSourceUri(final JsonMetaSchema metaSchema, final JsonNode schema, final URI schemaUri) { + private boolean idMatchesSourceUri(final JsonMetaSchema metaSchema, final JsonNode schema, final SchemaLocation schemaUri) { String id = metaSchema.readId(schema); if (id == null || id.isEmpty()) { return false; @@ -512,8 +375,8 @@ private boolean idMatchesSourceUri(final JsonMetaSchema metaSchema, final JsonNo return result; } - private boolean isYaml(final URI schemaUri) { - final String schemeSpecificPart = schemaUri.getSchemeSpecificPart(); + private boolean isYaml(final SchemaLocation schemaUri) { + final String schemeSpecificPart = schemaUri.toString(); final int idx = schemeSpecificPart.lastIndexOf('.'); if (idx == -1) { diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 1e5c74ee5..7ee733511 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -18,12 +18,9 @@ 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; import org.slf4j.LoggerFactory; -import java.net.URI; import java.util.Arrays; import java.util.Collections; import java.util.Set; @@ -58,25 +55,13 @@ 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); - if (schemaUri == null) { - // the URNFactory is optional - if (validationContext.getURNFactory() == null) { - return null; - } - // If the uri dose't determinate try to determinate with urn factory - schemaUri = determineSchemaUrn(validationContext.getURNFactory(), refUri); - if (schemaUri == null) { - return null; - } - } - - URI schemaUriFinal = schemaUri; + SchemaLocation schemaLocation = parentSchema.getSchemaLocation().resolve(refUri); + String schemaUriFinal = schemaLocation.toString(); // This should retrieve schemas regardless of the protocol that is in the uri. return new JsonSchemaRef(new CachedSupplier<>(() -> { JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaUriFinal.toString()); if (schemaResource == null) { - schemaResource = validationContext.getJsonSchemaFactory().getSchema(schemaUriFinal, validationContext.getConfig()); + schemaResource = validationContext.getJsonSchemaFactory().getSchema(schemaLocation, validationContext.getConfig()); if (schemaResource != null) { if (!schemaResource.getValidationContext().getSchemaResources().isEmpty()) { validationContext.getSchemaResources() @@ -110,7 +95,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val if (parentSchema.getId() != null && parentSchema.parentSchema != null) { base = parentSchema.parentSchema; } - if (base.getCurrentUri() != null) { + if (base.getSchemaLocation() != null) { String absoluteIri = SchemaLocation.resolve(base.getSchemaLocation(), refValue); // Schema resource needs to update the parent and evaluation path return new JsonSchemaRef(new CachedSupplier<>(() -> { @@ -154,7 +139,7 @@ private static JsonSchema getJsonSchema(JsonNode node, JsonSchema parent, if (node != null) { SchemaLocation path = null; JsonSchema currentParent = parent; - URI currentUri = parent.getCurrentUri(); + SchemaLocation currentUri = parent.getSchemaLocation(); if (refValue.startsWith(REF_CURRENT)) { // relative to document path = new SchemaLocation(parent.schemaLocation.getAbsoluteIri(), @@ -168,12 +153,12 @@ private static JsonSchema getJsonSchema(JsonNode node, JsonSchema parent, if (id != null) { if (id.contains(":")) { // absolute - currentUri = URI.create(id); + currentUri = currentUri.resolve(id); path = SchemaLocation.of(id); } else { // relative String absoluteUri = path.getAbsoluteIri().resolve(id).toString(); - currentUri = URI.create(absoluteUri); + currentUri = currentUri.resolve(absoluteUri); path = SchemaLocation.of(absoluteUri); } } @@ -194,38 +179,11 @@ private static JsonSchema getJsonSchema(JsonNode node, JsonSchema parent, path = path.append(parts[x]); } } - return validationContext.newSchema(path, evaluationPath, currentUri, node, currentParent); + return validationContext.newSchema(path, evaluationPath, node, currentParent); } throw null; } - private static URI determineSchemaUri(final URIFactory uriFactory, final JsonSchema parentSchema, final String refUri) { - URI schemaUri; - // $ref prevents a sibling $id from changing the base uri - JsonSchema parent = parentSchema.getParentSchema(); // just the parentSchema is the sibling $id with this $ref - final URI currentUri = parent != null ? parent.getCurrentUri() : parentSchema.getCurrentUri(); - try { - if (currentUri == null) { - schemaUri = uriFactory.create(refUri); - } else { - schemaUri = uriFactory.create(currentUri, refUri); - } - } catch (IllegalArgumentException e) { - schemaUri = null; - } - return schemaUri; - } - - private static URI determineSchemaUrn(final URNFactory urnFactory, final String refUri) { - URI schemaUrn; - try { - schemaUrn = urnFactory.create(refUri); - } catch (IllegalArgumentException e) { - schemaUrn = null; - } - return schemaUrn; - } - @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { CollectorContext collectorContext = executionContext.getCollectorContext(); diff --git a/src/main/java/com/networknt/schema/SchemaLocation.java b/src/main/java/com/networknt/schema/SchemaLocation.java index c0ecda4d3..8daf9c6b1 100644 --- a/src/main/java/com/networknt/schema/SchemaLocation.java +++ b/src/main/java/com/networknt/schema/SchemaLocation.java @@ -130,6 +130,9 @@ public static SchemaLocation of(String iri) { * @return the resolved schema location */ public SchemaLocation resolve(String absoluteIriReferenceOrFragment) { + if (absoluteIriReferenceOrFragment == null) { + return this; + } if ("#".equals(absoluteIriReferenceOrFragment)) { return new SchemaLocation(this.getAbsoluteIri(), JSON_POINTER); } diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index 3f90acaa4..9c7655542 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -19,8 +19,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.i18n.DefaultMessageSource; import com.networknt.schema.i18n.MessageSource; -import com.networknt.schema.uri.URITranslator; -import com.networknt.schema.uri.URITranslator.CompositeURITranslator; +import com.networknt.schema.uri.AbsoluteIriMapper; +import com.networknt.schema.uri.ClasspathSchemaLoader; +import com.networknt.schema.uri.DefaultSchemaLoader; +import com.networknt.schema.uri.SchemaLoader; +import com.networknt.schema.uri.UriSchemaLoader; import com.networknt.schema.walk.JsonSchemaWalkListener; import java.util.ArrayList; @@ -89,16 +92,6 @@ public class SchemaValidatorsConfig { */ private final Map strictness = new HashMap<>(0); - /** - * Map of public, normally internet accessible schema URLs to alternate - * locations; this allows for offline validation of schemas that refer to public - * URLs. This is merged with any mappings the {@link JsonSchemaFactory} may have - * been built with. - */ - private Map uriMappings = new HashMap<>(); - - private CompositeURITranslator uriTranslators = new CompositeURITranslator(); - /** * When a field is set as nullable in the OpenAPI specification, the schema * validator validates that it is nullable however continues with validation @@ -154,7 +147,7 @@ public class SchemaValidatorsConfig { // 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(); @@ -242,36 +235,6 @@ public ApplyDefaultsStrategy getApplyDefaultsStrategy() { return this.applyDefaultsStrategy; } - public CompositeURITranslator getUriTranslator() { - return this.uriTranslators - .with(URITranslator.map(this.uriMappings)); - } - - public void addUriTranslator(URITranslator uriTranslator) { - if (null != uriTranslator) { - this.uriTranslators.add(uriTranslator); - } - } - - /** - * @deprecated Use {@code getUriTranslator()} instead - * @return Map of public, normally internet accessible schema URLs - */ - @Deprecated - public Map getUriMappings() { - // return a copy of the mappings - return new HashMap<>(this.uriMappings); - } - - /** - * @deprecated Use {@code addUriTranslator()} instead - * @param uriMappings Map of public, normally internet accessible schema URLs - */ - @Deprecated - public void setUriMappings(Map uriMappings) { - this.uriMappings = uriMappings; - } - public boolean isHandleNullableField() { return this.handleNullableField; } diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java index 1e465b05f..31284fcb6 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -16,45 +16,34 @@ package com.networknt.schema; -import java.net.URI; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.SpecVersion.VersionFlag; -import com.networknt.schema.uri.URIFactory; -import com.networknt.schema.urn.URNFactory; public class ValidationContext { - private final URIFactory uriFactory; - private final URNFactory urnFactory; private final JsonMetaSchema metaSchema; private final JsonSchemaFactory jsonSchemaFactory; private final SchemaValidatorsConfig config; private final ConcurrentMap schemaReferences; private final ConcurrentMap schemaResources; - public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaSchema metaSchema, - JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config) { - this(uriFactory, urnFactory, metaSchema, jsonSchemaFactory, config, new ConcurrentHashMap<>(), - new ConcurrentHashMap<>()); + public ValidationContext(JsonMetaSchema metaSchema, + JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config) { + this(metaSchema, jsonSchemaFactory, config, new ConcurrentHashMap<>(), new ConcurrentHashMap<>()); } - - public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaSchema metaSchema, - JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config, - ConcurrentMap schemaReferences, ConcurrentMap schemaResources) { - if (uriFactory == null) { - throw new IllegalArgumentException("URIFactory must not be null"); - } + + public ValidationContext(JsonMetaSchema metaSchema, JsonSchemaFactory jsonSchemaFactory, + SchemaValidatorsConfig config, ConcurrentMap schemaReferences, + ConcurrentMap schemaResources) { if (metaSchema == null) { throw new IllegalArgumentException("JsonMetaSchema must not be null"); } if (jsonSchemaFactory == null) { throw new IllegalArgumentException("JsonSchemaFactory must not be null"); } - this.uriFactory = uriFactory; - this.urnFactory = urnFactory; this.metaSchema = metaSchema; this.jsonSchemaFactory = jsonSchemaFactory; this.config = config == null ? new SchemaValidatorsConfig() : config; @@ -66,10 +55,6 @@ public JsonSchema newSchema(SchemaLocation schemaLocation, JsonNodePath evaluati return getJsonSchemaFactory().create(this, schemaLocation, evaluationPath, schemaNode, parentSchema); } - public JsonSchema newSchema(SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, JsonNode schemaNode, JsonSchema parentSchema) { - return getJsonSchemaFactory().create(this, schemaLocation, evaluationPath, currentUri, schemaNode, parentSchema); - } - public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, String keyword /* keyword */, JsonNode schemaNode, JsonSchema parentSchema) { return this.metaSchema.newValidator(this, schemaLocation, evaluationPath, keyword, schemaNode, parentSchema); @@ -79,14 +64,6 @@ public String resolveSchemaId(JsonNode schemaNode) { return this.metaSchema.readId(schemaNode); } - public URIFactory getURIFactory() { - return this.uriFactory; - } - - public URNFactory getURNFactory() { - return this.urnFactory; - } - public JsonSchemaFactory getJsonSchemaFactory() { return this.jsonSchemaFactory; } diff --git a/src/main/java/com/networknt/schema/uri/URIFetcher.java b/src/main/java/com/networknt/schema/uri/AbsoluteIriMapper.java similarity index 66% rename from src/main/java/com/networknt/schema/uri/URIFetcher.java rename to src/main/java/com/networknt/schema/uri/AbsoluteIriMapper.java index 942117cd3..bed7d0dd1 100644 --- a/src/main/java/com/networknt/schema/uri/URIFetcher.java +++ b/src/main/java/com/networknt/schema/uri/AbsoluteIriMapper.java @@ -1,28 +1,26 @@ -/* - * Copyright (c) 2016 Network New Technologies Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.networknt.schema.uri; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; - -/** - * The URIFetcher interface defines how file streams are able to be fetched given a {@link URI}. - */ -public interface URIFetcher { - InputStream fetch(URI uri) throws IOException; -} +/* + * 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.uri; + +import com.networknt.schema.AbsoluteIri; + +/** + * Maps absolute IRI. + */ +@FunctionalInterface +public interface AbsoluteIriMapper { + AbsoluteIri map(AbsoluteIri absoluteIRI); +} diff --git a/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java b/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java new file mode 100644 index 000000000..c1f49cb3b --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java @@ -0,0 +1,48 @@ +/* + * 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.uri; + +import java.io.InputStream; + +import com.networknt.schema.SchemaLocation; + +/** + * Loads from classpath. + */ +public class ClasspathSchemaLoader implements SchemaLoader { + + @Override + public InputStreamSource getSchema(SchemaLocation schemaLocation) { + String scheme = schemaLocation.getAbsoluteIri().getScheme(); + if (scheme.startsWith("classpath") || scheme.startsWith("resource")) { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) { + classLoader = SchemaLoader.class.getClassLoader(); + } + ClassLoader loader = classLoader; + String name = schemaLocation.getAbsoluteIri().toString().substring(scheme.length() + 1); + return () -> { + InputStream result = loader.getResourceAsStream(name); + if (result == null) { + result = loader.getResourceAsStream(name.substring(1)); + } + return result; + }; + } + return null; + } + +} diff --git a/src/main/java/com/networknt/schema/uri/ClasspathURLFactory.java b/src/main/java/com/networknt/schema/uri/ClasspathURLFactory.java deleted file mode 100644 index 47e2fc862..000000000 --- a/src/main/java/com/networknt/schema/uri/ClasspathURLFactory.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2016 Network New Technologies Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.networknt.schema.uri; - -import java.net.*; -import java.util.Collections; -import java.util.Set; - -/** - * A URIFactory that uses URL for creating {@link URI}s. - */ -public final class ClasspathURLFactory implements URIFactory { - static final URLStreamHandler STREAM_HANDLER = new ClasspathURLStreamHandler(); - - public static final Set SUPPORTED_SCHEMES = Collections.unmodifiableSet( - ClasspathURLStreamHandler.SUPPORTED_SCHEMES); - - public static URL convert(final URI uri) throws MalformedURLException { - return new URL(null, uri.toString(), STREAM_HANDLER); - } - - /** - * {@inheritDoc} - */ - @Override - public URI create(final String uri) { - try { - return new URL(null, uri, STREAM_HANDLER).toURI(); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Unable to create URI.", e); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Unable to create URI.", e); - } - } - - /** - * {@inheritDoc} - */ - @Override - public URI create(final URI baseURI, final String segment) { - try { - return new URL(convert(baseURI), segment, STREAM_HANDLER).toURI(); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Unable to create URI.", e); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Unable to create URI.", e); - } - } -} diff --git a/src/main/java/com/networknt/schema/uri/ClasspathURLFetcher.java b/src/main/java/com/networknt/schema/uri/ClasspathURLFetcher.java deleted file mode 100644 index b93fe95e9..000000000 --- a/src/main/java/com/networknt/schema/uri/ClasspathURLFetcher.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2016 Network New Technologies Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.networknt.schema.uri; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URL; -import java.util.Collections; -import java.util.Set; - -/** - * A URIfetcher that uses {@link URL#openStream()} for fetching and assumes given {@link URI}s - * are actualy {@link URL}s. - */ -public final class ClasspathURLFetcher implements URIFetcher { - // This fetcher handles the {@link URL}s created with the {@link ClasspathURIFactory}. - public static final Set SUPPORTED_SCHEMES = Collections.unmodifiableSet(ClasspathURLFactory.SUPPORTED_SCHEMES); - - /** - * {@inheritDoc} - */ - @Override - public InputStream fetch(final URI uri) throws IOException { - return ClasspathURLFactory.convert(uri).openStream(); - } -} diff --git a/src/main/java/com/networknt/schema/uri/ClasspathURLStreamHandler.java b/src/main/java/com/networknt/schema/uri/ClasspathURLStreamHandler.java deleted file mode 100644 index dbe64975f..000000000 --- a/src/main/java/com/networknt/schema/uri/ClasspathURLStreamHandler.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2016 Network New Technologies Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.networknt.schema.uri; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * An {@link URLStreamHandler} capable of loading resources from the classpath. - * - * @author Kenneth Waldenstrom - */ -class ClasspathURLStreamHandler extends URLStreamHandler { - public static final Set SUPPORTED_SCHEMES = Collections.unmodifiableSet(new HashSet( - Arrays.asList("classpath", "resource"))); - - @Override - protected URLConnection openConnection(final URL pURL) throws IOException { - return new ClassPathURLConnection(pURL); - } - - class ClassPathURLConnection extends URLConnection { - - private Class mHost = null; - - protected ClassPathURLConnection(URL pURL) { - super(pURL); - } - - @Override - public final void connect() throws IOException { - String className = url.getHost(); - try { - if (className != null && className.length() > 0) { - mHost = Class.forName(className); - } - connected = true; - } catch (ClassNotFoundException e) { - throw new IOException("Class not found: " + e.toString()); - } - } - - @Override - public final InputStream getInputStream() throws IOException { - if (!connected) { - connect(); - } - return getResourceAsStream(url); - } - - private InputStream getResourceAsStream(URL pURL) throws IOException { - String path = pURL.getPath(); - - if (path.startsWith("/")) { - path = path.substring(1); - } - - InputStream stream = null; - if (mHost != null) { - stream = mHost.getClassLoader().getResourceAsStream(path); - } else { - ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); - if (contextClassLoader != null) { - stream = contextClassLoader.getResourceAsStream(path); - } - if (stream == null) { - stream = getClass().getClassLoader().getResourceAsStream(path); - } - if (stream == null) { - stream = ClassLoader.getSystemResourceAsStream(path); - } - } - if (stream == null) { - throw new IOException("Resource " + path + " not found in classpath."); - } - return stream; - } - } - - -} diff --git a/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java b/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java new file mode 100644 index 000000000..408fec9e3 --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java @@ -0,0 +1,55 @@ +/* + * 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.uri; + +import java.util.List; + +import com.networknt.schema.AbsoluteIri; +import com.networknt.schema.SchemaLocation; + +/** + * Default {@link SchemaLoader}. + */ +public class DefaultSchemaLoader implements SchemaLoader { + private final List schemaLoaders; + private final List absoluteIriMappers; + + public DefaultSchemaLoader(List schemaLoaders, List absoluteIriMappers) { + this.schemaLoaders = schemaLoaders; + this.absoluteIriMappers = absoluteIriMappers; + } + + @Override + public InputStreamSource getSchema(SchemaLocation schemaLocation) { + AbsoluteIri absoluteIri = schemaLocation.getAbsoluteIri(); + SchemaLocation mappedSchemaLocation = schemaLocation; + for (AbsoluteIriMapper mapper : absoluteIriMappers) { + AbsoluteIri mapped = mapper.map(absoluteIri); + if (mapped != null) { + mappedSchemaLocation = new SchemaLocation(mapped, schemaLocation.getFragment()); + break; + } + } + for (SchemaLoader loader : schemaLoaders) { + InputStreamSource result = loader.getSchema(mappedSchemaLocation); + if (result != null) { + return result; + } + } + return null; + } + +} diff --git a/src/main/java/com/networknt/schema/uri/InputStreamSource.java b/src/main/java/com/networknt/schema/uri/InputStreamSource.java new file mode 100644 index 000000000..149cb8235 --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/InputStreamSource.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 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.uri; + +import java.io.IOException; +import java.io.InputStream; + +/** + * InputStream source. + */ +@FunctionalInterface +public interface InputStreamSource { + /** + * Opens a new inputstream to the resource. + * + * @return a new inputstream + * @throws IOException + */ + InputStream getInputStream() throws IOException; +} diff --git a/src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java b/src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java new file mode 100644 index 000000000..3eaba98f5 --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java @@ -0,0 +1,26 @@ +package com.networknt.schema.uri; + +import java.util.Map; + +import com.networknt.schema.AbsoluteIri; + +/** + * Map implementation of {@link AbsoluteIriMapper}. + */ +public class MapAbsoluteIriMapper implements AbsoluteIriMapper { + private final Map mappings; + + public MapAbsoluteIriMapper(Map mappings) { + this.mappings = mappings; + } + + @Override + public AbsoluteIri map(AbsoluteIri absoluteIRI) { + String mapped = this.mappings.get(absoluteIRI.toString()); + if (mapped != null) { + return AbsoluteIri.of(mapped); + } + return null; + } + +} diff --git a/src/main/java/com/networknt/schema/uri/PrefixAbsoluteIriMapper.java b/src/main/java/com/networknt/schema/uri/PrefixAbsoluteIriMapper.java new file mode 100644 index 000000000..e5cdc1708 --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/PrefixAbsoluteIriMapper.java @@ -0,0 +1,25 @@ +package com.networknt.schema.uri; + +import com.networknt.schema.AbsoluteIri; + +/** + * Prefix implementation of {@link AbsoluteIriMapper}. + */ +public class PrefixAbsoluteIriMapper implements AbsoluteIriMapper { + private final String source; + private final String replacement; + + public PrefixAbsoluteIriMapper(String source, String replacement) { + this.source = source; + this.replacement = replacement; + } + + @Override + public AbsoluteIri map(AbsoluteIri absoluteIRI) { + String absoluteIRIString = absoluteIRI != null ? absoluteIRI.toString() : null; + if (absoluteIRIString != null && absoluteIRIString.startsWith(source)) { + return AbsoluteIri.of(replacement + absoluteIRIString.substring(source.length())); + } + return null; + } +} diff --git a/src/main/java/com/networknt/schema/uri/SchemaLoader.java b/src/main/java/com/networknt/schema/uri/SchemaLoader.java new file mode 100644 index 000000000..f9e27d5a5 --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/SchemaLoader.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 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.uri; + +import com.networknt.schema.SchemaLocation; + +/** + * Loader for schema. + */ +@FunctionalInterface +public interface SchemaLoader { + InputStreamSource getSchema(SchemaLocation schemaLocation); +} diff --git a/src/main/java/com/networknt/schema/uri/URIFactory.java b/src/main/java/com/networknt/schema/uri/URIFactory.java deleted file mode 100644 index ff5e35bcd..000000000 --- a/src/main/java/com/networknt/schema/uri/URIFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2016 Network New Technologies Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.networknt.schema.uri; - -import java.net.URI; - -/** - * The URIFactory interface defines how {@link URI}s are able to be combined and created. - */ -public interface URIFactory { - /** - * @param uri Some uri string. - * @return The converted {@link URI}. - * @throws IllegalArgumentException if there was a problem creating the {@link URI} with the given data. - */ - URI create(String uri); - - /** - * @param baseURI The base {@link URI}. - * @param segment The segment to add to the base {@link URI}. - * @return The combined {@link URI}. - * @throws IllegalArgumentException if there was a problem creating the {@link URI} with the given data. - */ - URI create(URI baseURI, String segment); -} diff --git a/src/main/java/com/networknt/schema/uri/URISchemeFactory.java b/src/main/java/com/networknt/schema/uri/URISchemeFactory.java deleted file mode 100644 index 3e7d5d3db..000000000 --- a/src/main/java/com/networknt/schema/uri/URISchemeFactory.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2016 Network New Technologies Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.networknt.schema.uri; - -import java.net.URI; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * The URISchemaFactory will proxy to other {@link URIFactory}s based on the scheme being used. - */ -public class URISchemeFactory implements URIFactory { - private static final Pattern URI_SCHEME_PATTERN = Pattern.compile("^([a-z][a-z0-9+\\.\\-\\\\]*):"); - - private final Map uriFactories; - - public URISchemeFactory(final Map uriFactories) { - if (uriFactories == null) { - throw new IllegalArgumentException("URIFactory map must not be null"); - } - this.uriFactories = uriFactories; - } - - public Map getURIFactories() { - return this.uriFactories; - } - - private static String getScheme(final String uri) { - final Matcher matcher = URI_SCHEME_PATTERN.matcher(uri); - if (matcher.find()) { - return matcher.group(1); - } - return null; - } - - private URIFactory getFactory(final String scheme) { - final URIFactory uriFactory = this.uriFactories.get(scheme); - if (uriFactory == null) { - throw new IllegalArgumentException(String.format("Unsupported URI scheme encountered: %s", scheme)); - } - return uriFactory; - } - - /** - * @param uri String - * @return URI - */ - @Override - public URI create(final String uri) { - final String scheme = getScheme(uri); - if (scheme == null) { - throw new IllegalArgumentException(String.format("Couldn't find URI scheme: %s", uri)); - } - - final URIFactory uriFactory = this.getFactory(scheme); - return uriFactory.create(uri); - } - - /** - * @param baseURI base URI - * @param segment URI segment - * @return URI - */ - @Override - public URI create(final URI baseURI, final String segment) { - if (baseURI == null) { - return this.create(segment); - } - - // We first attempt to get the scheme in case the segment is an absolute URI path. - String scheme = getScheme(segment); - if (scheme == null) { - // In this case, the segment is relative to the baseURI. - scheme = baseURI.getScheme(); - final URIFactory uriFactory = this.getFactory(scheme); - return uriFactory.create(baseURI, segment); - } - - if ("urn".equals(scheme)) { - return URI.create(segment); - } - - // In this case, the segment is an absolute URI path. - final URIFactory uriFactory = this.getFactory(scheme); - return uriFactory.create(segment); - } -} diff --git a/src/main/java/com/networknt/schema/uri/URISchemeFetcher.java b/src/main/java/com/networknt/schema/uri/URISchemeFetcher.java deleted file mode 100644 index 9e313d7d0..000000000 --- a/src/main/java/com/networknt/schema/uri/URISchemeFetcher.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2016 Network New Technologies Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.networknt.schema.uri; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.util.Map; - -/** - * The URISchemeFetcher will proxy to other {@link URIFetcher}s based on the scheme being used. - */ -public class URISchemeFetcher implements URIFetcher { - private final Map uriFetchers; - - public URISchemeFetcher(final Map uriFetchers) { - if (uriFetchers == null) { - throw new IllegalArgumentException("URIFetcher map must not be null"); - } - this.uriFetchers = uriFetchers; - } - - public Map getURIFetchers() { - return this.uriFetchers; - } - - /** - * @param uri URI - * @return InputStream - */ - public InputStream fetch(final URI uri) throws IOException { - final URIFetcher uriFetcher = this.uriFetchers.get(uri.getScheme()); - if (uriFetcher == null) { - throw new IllegalArgumentException(String.format("Unsupported URI scheme encountered: %s", uri.getScheme())); - } - return uriFetcher.fetch(uri); - } -} diff --git a/src/main/java/com/networknt/schema/uri/URITranslator.java b/src/main/java/com/networknt/schema/uri/URITranslator.java deleted file mode 100644 index aea812010..000000000 --- a/src/main/java/com/networknt/schema/uri/URITranslator.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.networknt.schema.uri; - -import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -@FunctionalInterface -public interface URITranslator { - static final URITranslator NOOP = original -> original; - - /** - * Translates one URI into another. - * @param original the URI to translate - * @return the translated URI or the original URI if it did not match - * the conditions triggering translation - */ - URI translate(URI original); - - /** - * Creates a simple mapping from one URI to another. - * @param source the URI to match - * @param target the URI to return when matched - * @return a new URITranslator - */ - static URITranslator map(String source, String target) { - return map(URI.create(source), URI.create(target)); - } - - /** - * Creates a simple mapping from one URI to another. - * @param source the URI to match - * @param target the URI to return when matched - * @return a new URITranslator - */ - static URITranslator map(URI source, URI target) { - return original -> Objects.equals(source, original) ? target : original; - } - - /** - * Creates a map-based mapping from one URI to another. - * @param uriMappings the mappings to build - * @return a new URITranslator - */ - static URITranslator map(Map uriMappings) { - return new MappingURITranslator(uriMappings); - } - - /** - * Creates a CompositeURITranslator. - * @param uriTranslators the translators to combine - * @return a new CompositeURITranslator - */ - static CompositeURITranslator combine(URITranslator... uriTranslators) { - return new CompositeURITranslator(uriTranslators); - } - - /** - * Creates a mapping from one URI to another by replacing the beginning of the URI. - *

- * For example, replace http with https. - * - * @param source the search string - * @param target the replacement string - * @return a new URITranslator - */ - static URITranslator prefix(String source, String target) { - return new PrefixReplacer(source, target); - } - - /** - * Creates a CompositeURITranslator. - * @param uriTranslators the translators to combine - * @return a new CompositeURITranslator - */ - static CompositeURITranslator combine(Collection uriTranslators) { - return new CompositeURITranslator(uriTranslators); - } - - class CompositeURITranslator extends ArrayList implements URITranslator { - private static final long serialVersionUID = 1L; - - public CompositeURITranslator() { - super(); - } - - public CompositeURITranslator(URITranslator...translators) { - this(Arrays.asList(translators)); - } - - public CompositeURITranslator(Collection c) { - super(c); - } - - @Override - public URI translate(URI original) { - URI result = original; - for (URITranslator translator: this) { - result = translator.translate(result); - } - return result; - } - - public CompositeURITranslator with(URITranslator translator) { - if (null != translator) { - return new CompositeURITranslator(this, translator); - } - return this; - } - } - - /** - * Provides support for legacy map-based translations - */ - class MappingURITranslator implements URITranslator { - private final Map mappings; - - public MappingURITranslator(Map uriMappings) { - this.mappings = new HashMap<>(); - if (null != uriMappings) { - uriMappings.forEach((k, v) -> this.mappings.put(URI.create(k), URI.create(v))); - } - } - - @Override - public URI translate(URI original) { - return this.mappings.getOrDefault(original, original); - } - - } - - /** - * Replaces the beginning of a URI - */ - class PrefixReplacer implements URITranslator { - private final String src; - private final String tgt; - - public PrefixReplacer(String src, String tgt) { - this.src = src; - this.tgt = tgt; - } - - @Override - public URI translate(URI original) { - if (null != original) { - String o = original.toString(); - if (o.startsWith(src)) { - o = tgt + o.substring(src.length()); - return URI.create(o); - } - } - - return original; - } - - } -} diff --git a/src/main/java/com/networknt/schema/uri/URLFactory.java b/src/main/java/com/networknt/schema/uri/URLFactory.java deleted file mode 100644 index 61fa56d1a..000000000 --- a/src/main/java/com/networknt/schema/uri/URLFactory.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2016 Network New Technologies Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.networknt.schema.uri; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * A URIFactory that uses URL for creating {@link URI}s. - */ -public final class URLFactory implements URIFactory { - // These supported schemes are defined in {@link #URL(String, String, int, String)}. - public static final Set SUPPORTED_SCHEMES = Collections.unmodifiableSet(new HashSet<>( - Arrays.asList("http", "https", "ftp", "file", "jar"))); - - /** - * @param uri String - * @return URI - */ - @Override - public URI create(final String uri) { - try { - return URI.create(uri); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Unable to create URI.", e); - } - } - - /** - * @param baseURI URI - * @param segment String - * @return URI - */ - @Override - public URI create(final URI baseURI, final String segment) { - try { - return new URL(baseURI.toURL(), segment).toURI(); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Unable to create URI.", e); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Unable to create URI.", e); - } - } -} diff --git a/src/main/java/com/networknt/schema/uri/URLFetcher.java b/src/main/java/com/networknt/schema/uri/URLFetcher.java deleted file mode 100644 index d5bb8d944..000000000 --- a/src/main/java/com/networknt/schema/uri/URLFetcher.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2016 Network New Technologies Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.networknt.schema.uri; - -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.net.URLConnection; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * A URIfetcher that uses {@link URL#openStream()} for fetching and assumes given {@link URI}s are actualy {@link URL}s. - */ -public final class URLFetcher implements URIFetcher { - - // These supported schemes are defined in {@link #URL(String, String, int, String)}. - // This fetcher also supports the {@link URL}s created with the {@link ClasspathURIFactory}. - public static final Set SUPPORTED_SCHEMES = URLFactory.SUPPORTED_SCHEMES; - - /** - * {@inheritDoc} - */ - @Override - public InputStream fetch(final URI uri) throws IOException { - URLConnection conn = uri.toURL().openConnection(); - return this.openConnectionCheckRedirects(conn); - } - - // https://www.cs.mun.ca/java-api-1.5/guide/deployment/deployment-guide/upgrade-guide/article-17.html - private InputStream openConnectionCheckRedirects(URLConnection c) throws IOException { - boolean redir; - int redirects = 0; - InputStream in = null; - do { - if (c instanceof HttpURLConnection) { - ((HttpURLConnection) c).setInstanceFollowRedirects(false); - } - // We want to open the input stream before getting headers - // because getHeaderField() et al swallow IOExceptions. - in = c.getInputStream(); - redir = false; - if (c instanceof HttpURLConnection) { - HttpURLConnection http = (HttpURLConnection) c; - int stat = http.getResponseCode(); - if (stat >= 300 && stat <= 307 && stat != 306 - && stat != HttpURLConnection.HTTP_NOT_MODIFIED) { - URL base = http.getURL(); - String loc = http.getHeaderField("Location"); - URL target = null; - if (loc != null) { - target = new URL(base, loc); - } - http.disconnect(); - // Redirection should be allowed only for HTTP and HTTPS - // and should be limited to 5 redirections at most. - if (target == null - || !(target.getProtocol().equals("http") - || target.getProtocol().equals("https")) - || redirects >= 5) { - throw new SecurityException("illegal URL redirect"); - } - redir = true; - c = target.openConnection(); - redirects++; - } - } - } - while (redir); - return in; - } -} diff --git a/src/main/java/com/networknt/schema/uri/URNURIFactory.java b/src/main/java/com/networknt/schema/uri/URNURIFactory.java deleted file mode 100644 index 8ea7914a3..000000000 --- a/src/main/java/com/networknt/schema/uri/URNURIFactory.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.networknt.schema.uri; - -import java.net.URI; -import java.util.Collections; -import java.util.Set; - -/** - * A URIFactory that handles "urn" scheme of {@link URI}s. - */ -public final class URNURIFactory implements URIFactory { - - public static final String SCHEME = "urn"; - - @Override - public URI create(final String uri) { - try { - return URI.create(uri); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Unable to create URI.", e); - } - } - - @Override - public URI create(final URI baseURI, final String segment) { - String urnPart = baseURI.getRawSchemeSpecificPart(); - int pos = urnPart.indexOf(':'); - String namespace = pos < 0 ? urnPart : urnPart.substring(0, pos); - return URI.create(SCHEME + ":" + namespace + ":" + segment); - } -} diff --git a/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java b/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java new file mode 100644 index 000000000..bf69037df --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java @@ -0,0 +1,31 @@ +/* + * 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.uri; + +import java.net.URI; + +import com.networknt.schema.SchemaLocation; + +/** + * Loads from uri. + */ +public class UriSchemaLoader implements SchemaLoader { + @Override + public InputStreamSource getSchema(SchemaLocation schemaLocation) { + URI uri = URI.create(schemaLocation.getAbsoluteIri().toString()); + return () -> uri.toURL().openStream(); + } +} diff --git a/src/main/java/com/networknt/schema/walk/WalkEvent.java b/src/main/java/com/networknt/schema/walk/WalkEvent.java index 9569e9e6a..bf7cf6ca6 100644 --- a/src/main/java/com/networknt/schema/walk/WalkEvent.java +++ b/src/main/java/com/networknt/schema/walk/WalkEvent.java @@ -9,8 +9,6 @@ import com.networknt.schema.SchemaValidatorsConfig; import com.networknt.schema.ValidationContext; -import java.net.URI; - /** * Encapsulation of Walk data that is passed into the {@link JsonSchemaWalkListener}. */ @@ -64,11 +62,11 @@ public JsonNodePath getInstanceLocation() { return instanceLocation; } - public JsonSchema getRefSchema(URI schemaUri) { + public JsonSchema getRefSchema(SchemaLocation schemaUri) { return currentJsonSchemaFactory.getSchema(schemaUri, validationContext.getConfig()); } - public JsonSchema getRefSchema(URI schemaUri, SchemaValidatorsConfig schemaValidatorsConfig) { + public JsonSchema getRefSchema(SchemaLocation schemaUri, SchemaValidatorsConfig schemaValidatorsConfig) { if (schemaValidatorsConfig != null) { return currentJsonSchemaFactory.getSchema(schemaUri, schemaValidatorsConfig); } else { diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java index f5d072196..f95be7dfa 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java @@ -21,14 +21,14 @@ import com.networknt.schema.suite.TestCase; import com.networknt.schema.suite.TestSource; import com.networknt.schema.suite.TestSpec; -import com.networknt.schema.uri.URITranslator; +import com.networknt.schema.uri.PrefixAbsoluteIriMapper; + import org.junit.jupiter.api.AssertionFailureBuilder; import org.junit.jupiter.api.DynamicNode; import org.opentest4j.AssertionFailedError; import java.io.IOException; import java.io.UncheckedIOException; -import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -184,10 +184,10 @@ private JsonSchemaFactory buildValidatorFactory(VersionFlag defaultVersion, Test return JsonSchemaFactory .builder(base) .objectMapper(this.mapper) - .addUriTranslator(URITranslator.combine( - URITranslator.prefix("https://", "http://"), - URITranslator.prefix("http://json-schema.org", "resource:") - )) + .absoluteIriMappers(absoluteIriMappers -> { + absoluteIriMappers.add(new PrefixAbsoluteIriMapper("https://", "http://")); + absoluteIriMappers.add(new PrefixAbsoluteIriMapper("http://json-schema.org", "resource:")); + }) .build(); } @@ -217,7 +217,7 @@ private DynamicNode buildTest(JsonSchemaFactory validatorFactory, TestSpec testS } } - URI testCaseFileUri = URI.create("classpath:" + toForwardSlashPath(testSpec.getTestCase().getSpecification())); + SchemaLocation testCaseFileUri = SchemaLocation.of("classpath:" + toForwardSlashPath(testSpec.getTestCase().getSpecification())); JsonSchema schema = validatorFactory.getSchema(testCaseFileUri, testSpec.getTestCase().getSchema(), config); return dynamicTest(testSpec.getDescription(), () -> executeAndReset(schema, testSpec)); diff --git a/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java b/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java index 3fc48fe48..ad74d5e32 100644 --- a/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java +++ b/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.io.InputStream; -import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -68,7 +67,7 @@ public static JsonSchema getJsonSchemaFromStringContent(String schemaContent) { public static JsonSchema getJsonSchemaFromUrl(String uri) throws URISyntaxException { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - return factory.getSchema(new URI(uri)); + return factory.getSchema(SchemaLocation.of(uri)); } public static JsonSchema getJsonSchemaFromJsonNode(JsonNode jsonNode) { diff --git a/src/test/java/com/networknt/schema/CustomUriTest.java b/src/test/java/com/networknt/schema/CustomUriTest.java index ded82508b..3df482c01 100644 --- a/src/test/java/com/networknt/schema/CustomUriTest.java +++ b/src/test/java/com/networknt/schema/CustomUriTest.java @@ -1,18 +1,11 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.SpecVersion; -import com.networknt.schema.ValidationMessage; -import com.networknt.schema.uri.URIFactory; -import com.networknt.schema.uri.URIFetcher; +import com.networknt.schema.uri.InputStreamSource; +import com.networknt.schema.uri.SchemaLoader; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Set; @@ -38,27 +31,15 @@ public void customUri() throws Exception { private JsonSchemaFactory buildJsonSchemaFactory() { return JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)) - .uriFetcher(new CustomUriFetcher(), "custom").uriFactory(new CustomUriFactory(), "custom").build(); + .schemaLoaders(schemaLoaders -> schemaLoaders.add(0, new CustomUriFetcher())).build(); } - private static class CustomUriFetcher implements URIFetcher { + private static class CustomUriFetcher implements SchemaLoader { private static final String SCHEMA = "{\"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\"$id\":\"custom:date\",\"type\":\"string\",\"format\":\"date\"}"; @Override - public InputStream fetch(final URI uri) throws IOException { - return new ByteArrayInputStream(SCHEMA.getBytes(StandardCharsets.UTF_8)); - } - } - - private static class CustomUriFactory implements URIFactory { - @Override - public URI create(final String uri) { - return URI.create(uri); - } - - @Override - public URI create(final URI baseURI, final String segment) { - return baseURI.resolve(segment); + public InputStreamSource getSchema(SchemaLocation schemaLocation) { + return () -> new ByteArrayInputStream(SCHEMA.getBytes(StandardCharsets.UTF_8)); } } } diff --git a/src/test/java/com/networknt/schema/CyclicDependencyTest.java b/src/test/java/com/networknt/schema/CyclicDependencyTest.java index 017a30862..8dd294d67 100644 --- a/src/test/java/com/networknt/schema/CyclicDependencyTest.java +++ b/src/test/java/com/networknt/schema/CyclicDependencyTest.java @@ -3,8 +3,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; -import java.net.URI; - import static org.junit.jupiter.api.Assertions.assertEquals; public class CyclicDependencyTest { @@ -34,9 +32,8 @@ public void whenDependencyBetweenSchemaThenValidationSuccessful() throws Excepti " ]\n" + "}"; - URI jsonSchemaLocation = URI.create("resource:/draft4/issue258/Master.json"); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - JsonSchema schema = schemaFactory.getSchema(jsonSchemaLocation, config); + JsonSchema schema = schemaFactory.getSchema(SchemaLocation.of("resource:/draft4/issue258/Master.json"), config); assertEquals(0, schema.validate(new ObjectMapper().readTree(jsonObject)).size()); } diff --git a/src/test/java/com/networknt/schema/Issue285Test.java b/src/test/java/com/networknt/schema/Issue285Test.java index 02257a127..2c1b6980b 100644 --- a/src/test/java/com/networknt/schema/Issue285Test.java +++ b/src/test/java/com/networknt/schema/Issue285Test.java @@ -1,13 +1,12 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.uri.URITranslator; +import com.networknt.schema.uri.PrefixAbsoluteIriMapper; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; import java.util.Set; @@ -19,10 +18,10 @@ public class Issue285Test { private JsonSchemaFactory schemaFactory = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)) .objectMapper(mapper) - .addUriTranslator(URITranslator.combine( - URITranslator.prefix("http://json-schema.org", "resource:"), - URITranslator.prefix("https://json-schema.org", "resource:") - )) + .absoluteIriMappers(absoluteIriMappers -> { + absoluteIriMappers.add(new PrefixAbsoluteIriMapper("http://json-schema.org", "resource:")); + absoluteIriMappers.add(new PrefixAbsoluteIriMapper("https://json-schema.org", "resource:")); + }) .build(); @@ -101,9 +100,8 @@ public void nestedValidation() throws IOException { // In this case a nested type declaration isn't valid and should raise an error. // The result is not as expected and we get no validation error. @Test - @Disabled public void nestedTypeValidation() throws IOException, URISyntaxException { - URI uri = new URI("https://json-schema.org/draft/2019-09/schema"); + SchemaLocation uri = SchemaLocation.of("https://json-schema.org/draft/2019-09/schema"); JsonSchema jsonSchema = schemaFactory.getSchema(uri); Set validationMessages = jsonSchema.validate(mapper.readTree(invalidNestedSchema)); @@ -126,7 +124,7 @@ public void nestedTypeValidation() throws IOException, URISyntaxException { // The result is as expected and we get no validation error: '[$.type: does not have a value in the enumeration [array, boolean, integer, null, number, object, string], $.type: should be valid to any of the schemas array]'. @Test public void typeValidation() throws IOException, URISyntaxException { - URI uri = new URI("https://json-schema.org/draft/2019-09/schema"); + SchemaLocation uri = SchemaLocation.of("https://json-schema.org/draft/2019-09/schema"); JsonSchema jsonSchema = schemaFactory.getSchema(uri); Set validationMessages = jsonSchema.validate(mapper.readTree(invalidSchema)); diff --git a/src/test/java/com/networknt/schema/Issue314Test.java b/src/test/java/com/networknt/schema/Issue314Test.java index 5260d3b89..1ac0200ee 100644 --- a/src/test/java/com/networknt/schema/Issue314Test.java +++ b/src/test/java/com/networknt/schema/Issue314Test.java @@ -12,7 +12,6 @@ public class Issue314Test { "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0", JsonMetaSchema.getV7()) .build()) - .forceHttps(false) .build(); @Test diff --git a/src/test/java/com/networknt/schema/Issue366FailFastTest.java b/src/test/java/com/networknt/schema/Issue366FailFastTest.java index 051159765..6267f51f1 100644 --- a/src/test/java/com/networknt/schema/Issue366FailFastTest.java +++ b/src/test/java/com/networknt/schema/Issue366FailFastTest.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; -import java.net.URI; import java.util.List; import java.util.Set; import org.junit.jupiter.api.BeforeEach; @@ -33,7 +32,7 @@ private void setupSchema() throws IOException { schemaValidatorsConfig.setTypeLoose(false); - URI uri = getSchema(); + SchemaLocation uri = getSchema(); InputStream in = getClass().getResourceAsStream("/schema/issue366_schema.json"); JsonNode testCases = objectMapper.readValue(in, JsonNode.class); @@ -100,7 +99,7 @@ public void neitherValid() throws Exception { }); } - private URI getSchema() { - return URI.create("classpath:" + "/draft7/issue366_schema.json"); + private SchemaLocation getSchema() { + return SchemaLocation.of("classpath:" + "/draft7/issue366_schema.json"); } } diff --git a/src/test/java/com/networknt/schema/Issue366FailSlowTest.java b/src/test/java/com/networknt/schema/Issue366FailSlowTest.java index 04773e794..4b6814909 100644 --- a/src/test/java/com/networknt/schema/Issue366FailSlowTest.java +++ b/src/test/java/com/networknt/schema/Issue366FailSlowTest.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; -import java.net.URI; import java.util.List; import java.util.Set; import org.junit.jupiter.api.BeforeEach; @@ -32,7 +31,7 @@ private void setupSchema() throws IOException { schemaValidatorsConfig.setTypeLoose(false); - URI uri = getSchema(); + SchemaLocation uri = getSchema(); InputStream in = getClass().getResourceAsStream("/schema/issue366_schema.json"); JsonNode testCases = objectMapper.readValue(in, JsonNode.class); @@ -99,7 +98,7 @@ public void neitherValid() throws Exception { assertEquals(errors.size(),3); } - private URI getSchema() { - return URI.create("classpath:" + "/draft7/issue366_schema.json"); + private SchemaLocation getSchema() { + return SchemaLocation.of("classpath:" + "/draft7/issue366_schema.json"); } } diff --git a/src/test/java/com/networknt/schema/Issue428Test.java b/src/test/java/com/networknt/schema/Issue428Test.java index f6ca37eed..5c9114329 100644 --- a/src/test/java/com/networknt/schema/Issue428Test.java +++ b/src/test/java/com/networknt/schema/Issue428Test.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test; import java.io.InputStream; -import java.net.URI; import java.util.ArrayList; import java.util.List; @@ -19,7 +18,7 @@ public class Issue428Test extends HTTPServiceSupport { .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)).objectMapper(mapper).build(); private void runTestFile(String testCaseFile) throws Exception { - final URI testCaseFileUri = URI.create("classpath:" + testCaseFile); + final SchemaLocation testCaseFileUri = SchemaLocation.of("classpath:" + testCaseFile); InputStream in = Thread.currentThread().getContextClassLoader() .getResourceAsStream(testCaseFile); ArrayNode testCases = mapper.readValue(in, ArrayNode.class); diff --git a/src/test/java/com/networknt/schema/Issue461Test.java b/src/test/java/com/networknt/schema/Issue461Test.java index 5328f65e9..239f61739 100644 --- a/src/test/java/com/networknt/schema/Issue461Test.java +++ b/src/test/java/com/networknt/schema/Issue461Test.java @@ -9,14 +9,13 @@ import org.junit.jupiter.api.Test; import java.io.IOException; -import java.net.URI; import java.net.URISyntaxException; import java.util.Set; public class Issue461Test { protected ObjectMapper mapper = new ObjectMapper(); - protected JsonSchema getJsonSchemaFromStreamContentV7(URI schemaUri) { + protected JsonSchema getJsonSchemaFromStreamContentV7(SchemaLocation schemaUri) { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); SchemaValidatorsConfig svc = new SchemaValidatorsConfig(); svc.addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(), new Walker()); @@ -25,7 +24,7 @@ protected JsonSchema getJsonSchemaFromStreamContentV7(URI schemaUri) { @Test public void shouldWalkWithValidation() throws URISyntaxException, IOException { - JsonSchema schema = getJsonSchemaFromStreamContentV7(new URI("resource:/draft-07/schema#")); + JsonSchema schema = getJsonSchemaFromStreamContentV7(SchemaLocation.of("resource:/draft-07/schema#")); JsonNode data = mapper.readTree(Issue461Test.class.getResource("/data/issue461-v7.json")); ValidationResult result = schema.walk(data, true); Assertions.assertTrue(result.getValidationMessages().isEmpty()); diff --git a/src/test/java/com/networknt/schema/Issue518Test.java b/src/test/java/com/networknt/schema/Issue518Test.java index 722e95d3b..7015eac23 100644 --- a/src/test/java/com/networknt/schema/Issue518Test.java +++ b/src/test/java/com/networknt/schema/Issue518Test.java @@ -15,8 +15,6 @@ public class Issue518Test { JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)) .addMetaSchema(igluMetaSchema) - .forceHttps(false) - .removeEmptyFragmentSuffix(false) .build(); @Test diff --git a/src/test/java/com/networknt/schema/Issue619Test.java b/src/test/java/com/networknt/schema/Issue619Test.java index 13e63efab..6c075d943 100644 --- a/src/test/java/com/networknt/schema/Issue619Test.java +++ b/src/test/java/com/networknt/schema/Issue619Test.java @@ -54,7 +54,7 @@ public void bundledSchemaLoadsAndValidatesCorrectly_Ref() { @Test public void bundledSchemaLoadsAndValidatesCorrectly_Uri() throws Exception { - JsonSchema rootSchema = factory.getSchema(new URI("resource:schema/issue619.json")); + JsonSchema rootSchema = factory.getSchema(SchemaLocation.of("resource:schema/issue619.json")); assertTrue(rootSchema.validate(one).isEmpty()); assertTrue(rootSchema.validate(two).isEmpty()); @@ -72,7 +72,7 @@ public void uriWithEmptyFragment_Ref() { @Test public void uriWithEmptyFragment_Uri() throws Exception { - JsonSchema rootSchema = factory.getSchema(new URI("resource:schema/issue619.json#")); + JsonSchema rootSchema = factory.getSchema(SchemaLocation.of("resource:schema/issue619.json#")); assertTrue(rootSchema.validate(one).isEmpty()); assertTrue(rootSchema.validate(two).isEmpty()); @@ -90,7 +90,7 @@ public void uriThatPointsToTwoShouldOnlyValidateTwo_Ref() { @Test public void uriThatPointsToOneShouldOnlyValidateOne_Uri() throws Exception { - JsonSchema oneSchema = factory.getSchema(new URI("resource:schema/issue619.json#/definitions/one")); + JsonSchema oneSchema = factory.getSchema(SchemaLocation.of("resource:schema/issue619.json#/definitions/one")); assertTrue(oneSchema.validate(one).isEmpty()); assertFalse(oneSchema.validate(two).isEmpty()); @@ -108,7 +108,7 @@ public void uriThatPointsToNodeThatInTurnReferencesOneShouldOnlyValidateOne_Ref( @Test public void uriThatPointsToNodeThatInTurnReferencesOneShouldOnlyValidateOne_Uri() throws Exception { - JsonSchema oneSchema = factory.getSchema(new URI("resource:schema/issue619.json#/definitions/refToOne")); + JsonSchema oneSchema = factory.getSchema(SchemaLocation.of("resource:schema/issue619.json#/definitions/refToOne")); assertTrue(oneSchema.validate(one).isEmpty()); assertFalse(oneSchema.validate(two).isEmpty()); @@ -130,7 +130,7 @@ public void uriThatPointsToSchemaWithIdThatHasDifferentUri_Uri() throws Exceptio JsonNode oneArray = getJsonNodeFromStringContent("[[1]]"); JsonNode textArray = getJsonNodeFromStringContent("[[\"a\"]]"); - JsonSchema schemaWithIdFromUri = factory.getSchema(new URI("resource:tests/draft4/refRemote.json#/3/schema")); + JsonSchema schemaWithIdFromUri = factory.getSchema(SchemaLocation.of("resource:tests/draft4/refRemote.json#/3/schema")); assertTrue(schemaWithIdFromUri.validate(oneArray).isEmpty()); assertFalse(schemaWithIdFromUri.validate(textArray).isEmpty()); } @@ -144,7 +144,7 @@ public void uriThatPointsToSchemaThatDoesNotExistShouldFail_Ref() { @Test public void uriThatPointsToSchemaThatDoesNotExistShouldFail_Uri() { - assertThrows(JsonSchemaException.class, () -> factory.getSchema(new URI("resource:data/schema-that-does-not-exist.json#/definitions/something"))); + assertThrows(JsonSchemaException.class, () -> factory.getSchema(SchemaLocation.of("resource:data/schema-that-does-not-exist.json#/definitions/something"))); } @Test @@ -156,6 +156,6 @@ public void uriThatPointsToNodeThatDoesNotExistShouldFail_Ref() { @Test public void uriThatPointsToNodeThatDoesNotExistShouldFail_Uri() { - assertThrows(JsonSchemaException.class, () -> factory.getSchema(new URI("resource:schema/issue619.json#/definitions/node-that-does-not-exist"))); + assertThrows(JsonSchemaException.class, () -> factory.getSchema(SchemaLocation.of("resource:schema/issue619.json#/definitions/node-that-does-not-exist"))); } } diff --git a/src/test/java/com/networknt/schema/Issue665Test.java b/src/test/java/com/networknt/schema/Issue665Test.java index 1b9f8bd04..2e4f2d680 100644 --- a/src/test/java/com/networknt/schema/Issue665Test.java +++ b/src/test/java/com/networknt/schema/Issue665Test.java @@ -3,10 +3,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import com.networknt.schema.uri.MapAbsoluteIriMapper; + import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; -import java.net.URI; import java.util.Collections; import java.util.Set; @@ -26,11 +27,10 @@ void testUrnUriAsLocalRef() throws IOException { void testUrnUriAsLocalRef_ExternalURN() { JsonSchemaFactory factory = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)) - .uriFetcher(uri -> uri.equals(URI.create("urn:data")) - ? Thread.currentThread().getContextClassLoader() - .getResourceAsStream("draft7/urn/issue665_external_urn_subschema.json") - : null, - "urn") + .absoluteIriMappers(absoluteIriMappers -> { + absoluteIriMappers.add(new MapAbsoluteIriMapper(Collections.singletonMap("urn:data", + "classpath:draft7/urn/issue665_external_urn_subschema.json"))); + }) .build(); try (InputStream is = Thread.currentThread().getContextClassLoader() diff --git a/src/test/java/com/networknt/schema/Issue824Test.java b/src/test/java/com/networknt/schema/Issue824Test.java index 164420b2d..eb483565b 100644 --- a/src/test/java/com/networknt/schema/Issue824Test.java +++ b/src/test/java/com/networknt/schema/Issue824Test.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.net.URI; import java.util.Set; import org.junit.jupiter.api.Test; @@ -10,15 +9,17 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.uri.URITranslator; +import com.networknt.schema.uri.PrefixAbsoluteIriMapper; public class Issue824Test { @Test void validate() throws JsonProcessingException { - SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - config.addUriTranslator(URITranslator.prefix("https://json-schema.org", "resource:")); - final JsonSchema v201909SpecSchema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909) - .getSchema(URI.create(JsonMetaSchema.getV201909().getUri()), config); + final JsonSchema v201909SpecSchema = JsonSchemaFactory + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)) + .absoluteIriMappers(absoluteIriMappers -> { + absoluteIriMappers.add(new PrefixAbsoluteIriMapper("https://json-schema.org", "resource:")); + }).build() + .getSchema(SchemaLocation.of(JsonMetaSchema.getV201909().getUri())); v201909SpecSchema.preloadJsonSchema(); final JsonNode invalidSchema = new ObjectMapper().readTree( "{"+ diff --git a/src/test/java/com/networknt/schema/Issue928Test.java b/src/test/java/com/networknt/schema/Issue928Test.java index 04e2d6651..778c2af95 100644 --- a/src/test/java/com/networknt/schema/Issue928Test.java +++ b/src/test/java/com/networknt/schema/Issue928Test.java @@ -1,12 +1,10 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.uri.URITranslator; +import com.networknt.schema.uri.PrefixAbsoluteIriMapper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import java.net.URI; - public class Issue928Test { private final ObjectMapper mapper = new ObjectMapper(); @@ -14,9 +12,7 @@ private JsonSchemaFactory factoryFor(SpecVersion.VersionFlag version) { return JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(version)) .objectMapper(mapper) - .addUriTranslator( - URITranslator.prefix("https://example.org", "resource:") - ) + .absoluteIriMappers(mappers -> mappers.add(new PrefixAbsoluteIriMapper("https://example.org", "classpath:"))) .build(); } @@ -45,13 +41,13 @@ public void test_spec(SpecVersion.VersionFlag specVersion) { System.out.println("baseUrl: " + baseUrl); JsonSchema byPointer = schemaFactory.getSchema( - URI.create(baseUrl + "#/definitions/example")); + SchemaLocation.of(baseUrl + "#/definitions/example")); Assertions.assertEquals(byPointer.validate(mapper.valueToTree("A")).size(), 0); Assertions.assertEquals(byPointer.validate(mapper.valueToTree("Z")).size(), 1); JsonSchema byAnchor = schemaFactory.getSchema( - URI.create(baseUrl + "#example")); + SchemaLocation.of(baseUrl + "#example")); Assertions.assertEquals( byPointer.getSchemaNode(), diff --git a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java index d3e382c3e..83aac6da4 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java @@ -2,15 +2,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.uri.ClasspathURLFactory; -import com.networknt.schema.uri.URIFetcher; -import com.networknt.schema.uri.URLFactory; +import com.networknt.schema.uri.InputStreamSource; +import com.networknt.schema.uri.SchemaLoader; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -20,7 +17,6 @@ public class JsonSchemaFactoryUriCacheTest { private final ObjectMapper objectMapper = new ObjectMapper(); - private final ClasspathURLFactory classpathURLFactory = new ClasspathURLFactory(); @Test public void cacheEnabled() throws JsonProcessingException { @@ -35,7 +31,7 @@ public void cacheDisabled() throws JsonProcessingException { private void runCacheTest(boolean enableCache) throws JsonProcessingException { CustomURIFetcher fetcher = new CustomURIFetcher(); JsonSchemaFactory factory = buildJsonSchemaFactory(fetcher, enableCache); - URI schemaUri = classpathURLFactory.create("cache:uri_mapping/schema1.json"); + SchemaLocation schemaUri = SchemaLocation.of("cache:uri_mapping/schema1.json"); String schema = "{ \"$schema\": \"https://json-schema.org/draft/2020-12/schema#\", \"title\": \"json-object-with-schema\", \"type\": \"string\" }"; fetcher.addResource(schemaUri, schema); assertEquals(objectMapper.readTree(schema), factory.getSchema(schemaUri, new SchemaValidatorsConfig()).schemaNode); @@ -49,27 +45,26 @@ private void runCacheTest(boolean enableCache) throws JsonProcessingException { private JsonSchemaFactory buildJsonSchemaFactory(CustomURIFetcher uriFetcher, boolean enableUriSchemaCache) { return JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)) .enableUriSchemaCache(enableUriSchemaCache) - .uriFetcher(uriFetcher, "cache") - .uriFactory(new URLFactory(), "cache") + .schemaLoaders(schemaLoaders -> schemaLoaders.add(0, uriFetcher)) .addMetaSchema(JsonMetaSchema.getV202012()) .build(); } - private class CustomURIFetcher implements URIFetcher { + private class CustomURIFetcher implements SchemaLoader { - private Map uriToResource = new HashMap<>(); + private Map uriToResource = new HashMap<>(); - void addResource(URI uri, String schema) { + void addResource(SchemaLocation uri, String schema) { addResource(uri, new ByteArrayInputStream(schema.getBytes(StandardCharsets.UTF_8))); } - void addResource(URI uri, InputStream is) { + void addResource(SchemaLocation uri, InputStream is) { uriToResource.put(uri, is); } @Override - public InputStream fetch(URI uri) throws IOException { - return uriToResource.get(uri); + public InputStreamSource getSchema(SchemaLocation schemaLocation) { + return () -> uriToResource.get(schemaLocation); } } } diff --git a/src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java b/src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java index 8c8006b18..3c7fae236 100644 --- a/src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java @@ -1,7 +1,6 @@ package com.networknt.schema; import java.io.InputStream; -import java.net.URI; import java.util.ArrayList; import java.util.List; @@ -22,7 +21,7 @@ public OpenAPI30JsonSchemaTest() { } private void runTestFile(String testCaseFile) throws Exception { - final URI testCaseFileUri = URI.create("classpath:" + testCaseFile); + final SchemaLocation testCaseFileUri = SchemaLocation.of("classpath:" + testCaseFile); InputStream in = Thread.currentThread().getContextClassLoader() .getResourceAsStream(testCaseFile); ArrayNode testCases = mapper.readValue(in, ArrayNode.class); diff --git a/src/test/java/com/networknt/schema/RecursiveReferenceValidatorExceptionTest.java b/src/test/java/com/networknt/schema/RecursiveReferenceValidatorExceptionTest.java index 819d122dd..1159a57ab 100644 --- a/src/test/java/com/networknt/schema/RecursiveReferenceValidatorExceptionTest.java +++ b/src/test/java/com/networknt/schema/RecursiveReferenceValidatorExceptionTest.java @@ -22,7 +22,8 @@ void testInvalidRecursiveReference() { JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); JsonSchema jsonSchema = jsonSchemaFactory.getSchema(invalidSchemaJson); JsonNode schemaNode = jsonSchema.getSchemaNode(); - ValidationContext validationContext = new ValidationContext(jsonSchemaFactory.getUriFactory(), null, jsonSchema.getValidationContext().getMetaSchema(), jsonSchemaFactory, null); + ValidationContext validationContext = new ValidationContext(jsonSchema.getValidationContext().getMetaSchema(), + jsonSchemaFactory, null); // Act and Assert assertThrows(JsonSchemaException.class, () -> { diff --git a/src/test/java/com/networknt/schema/SharedConfigTest.java b/src/test/java/com/networknt/schema/SharedConfigTest.java index 63e63ceb8..283d99985 100644 --- a/src/test/java/com/networknt/schema/SharedConfigTest.java +++ b/src/test/java/com/networknt/schema/SharedConfigTest.java @@ -37,7 +37,7 @@ public void shouldCallAllKeywordListenerOnWalkStart() throws Exception { AllKeywordListener allKeywordListener = new AllKeywordListener(); schemaValidatorsConfig.addKeywordWalkListener(allKeywordListener); - URI draft07Schema = new URI("resource:/draft-07/schema#"); + SchemaLocation draft07Schema = SchemaLocation.of("resource:/draft-07/schema#"); // depending on this line the test either passes or fails: // - if this line is executed, then it passes diff --git a/src/test/java/com/networknt/schema/UriMappingTest.java b/src/test/java/com/networknt/schema/UriMappingTest.java index a4f41786f..4eb37c049 100644 --- a/src/test/java/com/networknt/schema/UriMappingTest.java +++ b/src/test/java/com/networknt/schema/UriMappingTest.java @@ -18,15 +18,13 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.JsonSchemaFactory.Builder; -import com.networknt.schema.uri.ClasspathURLFactory; -import com.networknt.schema.uri.URITranslator; -import com.networknt.schema.uri.URLFactory; +import com.networknt.schema.uri.AbsoluteIriMapper; +import com.networknt.schema.uri.MapAbsoluteIriMapper; import org.junit.jupiter.api.Test; import java.io.FileNotFoundException; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; +import java.io.UncheckedIOException; import java.net.URL; import java.net.UnknownHostException; import java.util.HashMap; @@ -37,26 +35,6 @@ public class UriMappingTest { private final ObjectMapper mapper = new ObjectMapper(); - private final ClasspathURLFactory classpathURLFactory = new ClasspathURLFactory(); - private final URLFactory urlFactory = new URLFactory(); - - /** - * Validate URI Create API - */ - @Test - public void testUrlFactoryCreate() { - try { - this.urlFactory.create("://example.com/invalid/schema/url"); - fail("Invalid URI, should throw error."); - } - catch(IllegalArgumentException e){ - - } - catch(Exception e){ - fail("Unknown Exception occured "); - } - - } /** * Validate that a JSON URI Mapping file containing the URI Mapping schema is @@ -66,15 +44,14 @@ public void testUrlFactoryCreate() { */ @Test public void testBuilderUriMappingUri() throws IOException { - URL mappings = ClasspathURLFactory.convert( - this.classpathURLFactory.create("resource:draft4/extra/uri_mapping/uri-mapping.json")); + URL mappings = UriMappingTest.class.getResource("/draft4/extra/uri_mapping/uri-mapping.json"); JsonMetaSchema draftV4 = JsonMetaSchema.getV4(); Builder builder = JsonSchemaFactory.builder() .defaultMetaSchemaURI(draftV4.getUri()) .addMetaSchema(draftV4) - .addUriTranslator(getUriMappingsFromUrl(mappings)); + .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(getUriMappingsFromUrl(mappings))); JsonSchemaFactory instance = builder.build(); - JsonSchema schema = instance.getSchema(this.urlFactory.create( + JsonSchema schema = instance.getSchema(SchemaLocation.of( "https://raw.githubusercontent.com/networknt/json-schema-validator/master/src/test/resources/draft4/extra/uri_mapping/uri-mapping.schema.json")); assertEquals(0, schema.validate(mapper.readTree(mappings)).size()); } @@ -90,7 +67,7 @@ public void testBuilderUriMappingUri() throws IOException { @Test public void testBuilderExampleMappings() throws IOException { JsonSchemaFactory instance = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - URI example = this.urlFactory.create("http://example.com/invalid/schema/url"); + SchemaLocation example = SchemaLocation.of("http://example.com/invalid/schema/url"); // first test that attempting to use example URL throws an error try { JsonSchema schema = instance.getSchema(example); @@ -105,13 +82,12 @@ public void testBuilderExampleMappings() throws IOException { } catch (Exception ex) { fail("Unexpected exception thrown", ex); } - URL mappings = ClasspathURLFactory.convert( - this.classpathURLFactory.create("resource:draft4/extra/uri_mapping/invalid-schema-uri.json")); + URL mappings = UriMappingTest.class.getResource("/draft4/extra/uri_mapping/invalid-schema-uri.json"); JsonMetaSchema draftV4 = JsonMetaSchema.getV4(); Builder builder = JsonSchemaFactory.builder() .defaultMetaSchemaURI(draftV4.getUri()) .addMetaSchema(draftV4) - .addUriTranslator(getUriMappingsFromUrl(mappings)); + .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(getUriMappingsFromUrl(mappings))); instance = builder.build(); JsonSchema schema = instance.getSchema(example); assertEquals(0, schema.validate(mapper.createObjectNode()).size()); @@ -125,14 +101,11 @@ public void testBuilderExampleMappings() throws IOException { */ @Test public void testValidatorConfigUriMappingUri() throws IOException { - JsonSchemaFactory instance = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - URL mappings = ClasspathURLFactory.convert( - this.classpathURLFactory.create("resource:draft4/extra/uri_mapping/uri-mapping.json")); - SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - config.addUriTranslator(getUriMappingsFromUrl(mappings)); - JsonSchema schema = instance.getSchema(this.urlFactory.create( - "https://raw.githubusercontent.com/networknt/json-schema-validator/master/src/test/resources/draft4/extra/uri_mapping/uri-mapping.schema.json"), - config); + URL mappings = UriMappingTest.class.getResource("/draft4/extra/uri_mapping/uri-mapping.json"); + JsonSchemaFactory instance = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) + .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(getUriMappingsFromUrl(mappings))).build(); + JsonSchema schema = instance.getSchema(SchemaLocation.of( + "https://raw.githubusercontent.com/networknt/json-schema-validator/master/src/test/resources/draft4/extra/uri_mapping/uri-mapping.schema.json")); assertEquals(0, schema.validate(mapper.readTree(mappings)).size()); } @@ -146,9 +119,11 @@ public void testValidatorConfigUriMappingUri() throws IOException { */ @Test public void testValidatorConfigExampleMappings() throws IOException { - JsonSchemaFactory instance = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); + URL mappings = UriMappingTest.class.getResource("/draft4/extra/uri_mapping/invalid-schema-uri.json"); + JsonSchemaFactory instance = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) + .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(getUriMappingsFromUrl(mappings))).build(); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - URI example = this.urlFactory.create("http://example.com/invalid/schema/url"); + SchemaLocation example = SchemaLocation.of("http://example.com/invalid/schema/url"); // first test that attempting to use example URL throws an error try { JsonSchema schema = instance.getSchema(example, config); @@ -163,31 +138,31 @@ public void testValidatorConfigExampleMappings() throws IOException { } catch (Exception ex) { fail("Unexpected exception thrown"); } - URL mappings = ClasspathURLFactory.convert( - this.classpathURLFactory.create("resource:draft4/extra/uri_mapping/invalid-schema-uri.json")); - config.addUriTranslator(getUriMappingsFromUrl(mappings)); JsonSchema schema = instance.getSchema(example, config); assertEquals(0, schema.validate(mapper.createObjectNode()).size()); } @Test public void testMappingsForRef() throws IOException { - JsonSchemaFactory instance = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - URL mappings = ClasspathURLFactory.convert( - this.classpathURLFactory.create("resource:draft4/extra/uri_mapping/schema-with-ref-mapping.json")); + URL mappings = UriMappingTest.class.getResource("/draft4/extra/uri_mapping/schema-with-ref-mapping.json"); + JsonSchemaFactory instance = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) + .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(getUriMappingsFromUrl(mappings))).build(); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - config.addUriTranslator(getUriMappingsFromUrl(mappings)); - JsonSchema schema = instance.getSchema(this.classpathURLFactory.create("resource:draft4/extra/uri_mapping/schema-with-ref.json"), + JsonSchema schema = instance.getSchema(SchemaLocation.of("resource:draft4/extra/uri_mapping/schema-with-ref.json"), config); assertEquals(0, schema.validate(mapper.readTree("[]")).size()); } - private URITranslator getUriMappingsFromUrl(URL url) throws MalformedURLException, IOException { + private AbsoluteIriMapper getUriMappingsFromUrl(URL url) { HashMap map = new HashMap(); - for (JsonNode mapping : mapper.readTree(url)) { - map.put(mapping.get("publicURL").asText(), - mapping.get("localURL").asText()); + try { + for (JsonNode mapping : mapper.readTree(url)) { + map.put(mapping.get("publicURL").asText(), + mapping.get("localURL").asText()); + } + } catch (IOException e) { + throw new UncheckedIOException(e); } - return URITranslator.map(map); + return new MapAbsoluteIriMapper(map); } } diff --git a/src/test/java/com/networknt/schema/UrnTest.java b/src/test/java/com/networknt/schema/UrnTest.java index d28ec56e4..dc27aea52 100644 --- a/src/test/java/com/networknt/schema/UrnTest.java +++ b/src/test/java/com/networknt/schema/UrnTest.java @@ -1,14 +1,10 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.uri.ClasspathURLFactory; -import com.networknt.schema.uri.URLFactory; -import com.networknt.schema.urn.URNFactory; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.InputStream; -import java.net.URI; import java.net.URL; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -16,8 +12,6 @@ public class UrnTest { private final ObjectMapper mapper = new ObjectMapper(); - private final ClasspathURLFactory classpathURLFactory = new ClasspathURLFactory(); - private final URLFactory urlFactory = new URLFactory(); /** * Validate that a JSON URI Mapping file containing the URI Mapping schema is @@ -27,22 +21,7 @@ public class UrnTest */ @Test public void testURNToURI() throws Exception { - URL urlTestData = ClasspathURLFactory.convert( - this.classpathURLFactory.create("resource:draft7/urn/test.json")); - - URNFactory urnFactory = new URNFactory() - { - @Override public URI create(String urn) - { - try { - URL absoluteURL = ClasspathURLFactory.convert(new ClasspathURLFactory().create(String.format("resource:draft7/urn/%s.schema.json", urn))); - return absoluteURL.toURI(); - } catch (Exception ex) { - return null; - } - } - }; - + InputStream urlTestData = UrnTest.class.getResourceAsStream("/draft7/urn/test.json"); InputStream is = null; try { is = new URL("https://raw.githubusercontent.com/francesc79/json-schema-validator/feature/urn-management/src/test/resources/draft7/urn/urn.schema.json").openStream(); @@ -50,7 +29,8 @@ public void testURNToURI() throws Exception { JsonSchemaFactory.Builder builder = JsonSchemaFactory.builder() .defaultMetaSchemaURI(draftV7.getUri()) .addMetaSchema(draftV7) - .addUrnFactory(urnFactory); + .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(value -> AbsoluteIri.of(String.format("resource:draft7/urn/%s.schema.json", value.toString()))) + ); JsonSchemaFactory instance = builder.build(); JsonSchema schema = instance.getSchema(is); assertEquals(0, schema.validate(mapper.readTree(urlTestData)).size()); From e78bf1abf1976c4fd7df380f2808a4571dcda239 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:30:00 +0800 Subject: [PATCH 03/65] Refactor --- .../java/com/networknt/schema/JsonSchema.java | 3 +- .../networknt/schema/JsonSchemaFactory.java | 2 +- .../com/networknt/schema/RefValidator.java | 48 +++++++++---------- .../schema/uri/DefaultSchemaLoader.java | 9 ++-- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index fa05efcf6..9138c66fe 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -229,7 +229,8 @@ public boolean isSchemaResourceRoot() { return true; } // The schema should not cross - if (!getSchemaLocation().getAbsoluteIri().equals(getParentSchema().getSchemaLocation().getAbsoluteIri())) { + if (!Objects.equals(getSchemaLocation().getAbsoluteIri(), + getParentSchema().getSchemaLocation().getAbsoluteIri())) { return true; } return false; diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 67b795c22..734081041 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -376,7 +376,7 @@ private boolean idMatchesSourceUri(final JsonMetaSchema metaSchema, final JsonNo } private boolean isYaml(final SchemaLocation schemaUri) { - final String schemeSpecificPart = schemaUri.toString(); + final String schemeSpecificPart = schemaUri.getAbsoluteIri().toString(); final int idx = schemeSpecificPart.lastIndexOf('.'); if (idx == -1) { diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 7ee733511..80aada7d1 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -21,9 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Arrays; -import java.util.Collections; -import java.util.Set; +import java.util.*; public class RefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(RefValidator.class); @@ -55,8 +53,8 @@ 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. - SchemaLocation schemaLocation = parentSchema.getSchemaLocation().resolve(refUri); - String schemaUriFinal = schemaLocation.toString(); + String schemaUriFinal = resolve(parentSchema, refUri); + SchemaLocation schemaLocation = SchemaLocation.of(schemaUriFinal); // This should retrieve schemas regardless of the protocol that is in the uri. return new JsonSchemaRef(new CachedSupplier<>(() -> { JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaUriFinal.toString()); @@ -90,25 +88,18 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val })); } else if (SchemaLocation.Fragment.isAnchorFragment(refValue)) { - // $ref prevents a sibling $id from changing the base uri - JsonSchema base = parentSchema; - if (parentSchema.getId() != null && parentSchema.parentSchema != null) { - base = parentSchema.parentSchema; - } - if (base.getSchemaLocation() != null) { - String absoluteIri = SchemaLocation.resolve(base.getSchemaLocation(), refValue); - // Schema resource needs to update the parent and evaluation path - return new JsonSchemaRef(new CachedSupplier<>(() -> { - JsonSchema schemaResource = validationContext.getSchemaResources().get(absoluteIri); - if (schemaResource == null) { - schemaResource = getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath); - } - if (schemaResource == null) { - return null; - } - return schemaResource.fromRef(parentSchema, evaluationPath); - })); - } + String absoluteIri = resolve(parentSchema, refValue); + // Schema resource needs to update the parent and evaluation path + return new JsonSchemaRef(new CachedSupplier<>(() -> { + JsonSchema schemaResource = validationContext.getSchemaResources().get(absoluteIri); + if (schemaResource == null) { + schemaResource = getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath); + } + if (schemaResource == null) { + return null; + } + return schemaResource.fromRef(parentSchema, evaluationPath); + })); } if (refValue.equals(REF_CURRENT)) { return new JsonSchemaRef(new CachedSupplier<>( @@ -117,6 +108,15 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val return new JsonSchemaRef(new CachedSupplier<>( () -> getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath))); } + + private static String resolve(JsonSchema parentSchema, String refValue) { + // $ref prevents a sibling $id from changing the base uri + JsonSchema base = parentSchema; + if (parentSchema.getId() != null && parentSchema.parentSchema != null) { + base = parentSchema.parentSchema; + } + return SchemaLocation.resolve(base.getSchemaLocation(), refValue); + } private static JsonSchema getJsonSchema(JsonSchema parent, ValidationContext validationContext, diff --git a/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java b/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java index 408fec9e3..4707a9bab 100644 --- a/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java +++ b/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java @@ -35,14 +35,17 @@ public DefaultSchemaLoader(List schemaLoaders, List Date: Wed, 10 Jan 2024 15:04:59 +0800 Subject: [PATCH 04/65] Refactor --- .../networknt/schema/JsonSchemaFactory.java | 38 +++----- .../schema/uri/SchemaLoaderBuilder.java | 87 +++++++++++++++++++ .../schema/AbstractJsonSchemaTestSuite.java | 7 +- .../com/networknt/schema/CustomUriTest.java | 2 +- .../com/networknt/schema/Issue285Test.java | 9 +- .../com/networknt/schema/Issue665Test.java | 8 +- .../com/networknt/schema/Issue824Test.java | 5 +- .../schema/JsonSchemaFactoryUriCacheTest.java | 2 +- .../com/networknt/schema/UriMappingTest.java | 10 +-- .../java/com/networknt/schema/UrnTest.java | 2 +- 10 files changed, 117 insertions(+), 53 deletions(-) create mode 100644 src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 734081041..f8349965a 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -27,9 +27,7 @@ import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -45,15 +43,9 @@ public static class Builder { private YAMLMapper yamlMapper = null; private String defaultMetaSchemaURI; private final ConcurrentMap jsonMetaSchemas = new ConcurrentHashMap(); - private List schemaLoaders = new ArrayList<>(); - private List absoluteIriMappers = new ArrayList<>(); + private SchemaLoaderBuilder schemaLoaderBuilder = new SchemaLoaderBuilder(); private boolean enableUriSchemaCache = true; - public Builder() { - this.schemaLoaders.add(new ClasspathSchemaLoader()); - this.schemaLoaders.add(new UriSchemaLoader()); - } - public Builder objectMapper(final ObjectMapper objectMapper) { this.objectMapper = objectMapper; return this; @@ -86,13 +78,8 @@ public Builder enableUriSchemaCache(boolean enableUriSchemaCache) { return this; } - public Builder schemaLoaders(Consumer> schemaLoaderCustomizer) { - schemaLoaderCustomizer.accept(this.schemaLoaders); - return this; - } - - public Builder absoluteIriMappers(Consumer> absoluteIriCustomizer) { - absoluteIriCustomizer.accept(this.absoluteIriMappers); + public Builder schemaLoaderBuilder(Consumer schemaLoaderBuilderCustomizer) { + schemaLoaderBuilderCustomizer.accept(this.schemaLoaderBuilder); return this; } @@ -102,8 +89,7 @@ public JsonSchemaFactory build() { objectMapper == null ? new ObjectMapper() : objectMapper, yamlMapper == null ? new YAMLMapper(): yamlMapper, defaultMetaSchemaURI, - schemaLoaders, - absoluteIriMappers, + schemaLoaderBuilder, jsonMetaSchemas, enableUriSchemaCache ); @@ -113,8 +99,7 @@ public JsonSchemaFactory build() { private final ObjectMapper jsonMapper; private final YAMLMapper yamlMapper; private final String defaultMetaSchemaURI; - private final List schemaLoaders; - private final List absoluteIriMappers; + private final SchemaLoaderBuilder schemaLoaderBuilder; private final SchemaLoader schemaLoader; private final Map jsonMetaSchemas; private final ConcurrentMap uriSchemaCache = new ConcurrentHashMap<>(); @@ -125,8 +110,7 @@ private JsonSchemaFactory( final ObjectMapper jsonMapper, final YAMLMapper yamlMapper, final String defaultMetaSchemaURI, - List schemaLoaders, - final List absoluteIriMappers, + SchemaLoaderBuilder schemaLoaderBuilder, final Map jsonMetaSchemas, final boolean enableUriSchemaCache) { if (jsonMapper == null) { @@ -135,7 +119,7 @@ private JsonSchemaFactory( throw new IllegalArgumentException("YAMLMapper must not be null"); } else if (defaultMetaSchemaURI == null || defaultMetaSchemaURI.trim().isEmpty()) { throw new IllegalArgumentException("defaultMetaSchemaURI must not be null or empty"); - } else if (schemaLoaders == null) { + } else if (schemaLoaderBuilder == null) { throw new IllegalArgumentException("SchemaLoaders must not be null"); } else if (jsonMetaSchemas == null || jsonMetaSchemas.isEmpty()) { throw new IllegalArgumentException("Json Meta Schemas must not be null or empty"); @@ -145,9 +129,8 @@ private JsonSchemaFactory( this.jsonMapper = jsonMapper; this.yamlMapper = yamlMapper; this.defaultMetaSchemaURI = defaultMetaSchemaURI; - this.schemaLoaders = schemaLoaders; - this.absoluteIriMappers = absoluteIriMappers; - this.schemaLoader = new DefaultSchemaLoader(schemaLoaders, absoluteIriMappers); + this.schemaLoaderBuilder = schemaLoaderBuilder; + this.schemaLoader = schemaLoaderBuilder.build(); this.jsonMetaSchemas = jsonMetaSchemas; this.enableUriSchemaCache = enableUriSchemaCache; } @@ -208,8 +191,7 @@ public static Builder builder(final JsonSchemaFactory blueprint) { .defaultMetaSchemaURI(blueprint.defaultMetaSchemaURI) .objectMapper(blueprint.jsonMapper) .yamlMapper(blueprint.yamlMapper); - builder.schemaLoaders = blueprint.schemaLoaders; - builder.absoluteIriMappers = blueprint.absoluteIriMappers; + builder.schemaLoaderBuilder = blueprint.schemaLoaderBuilder; return builder; } diff --git a/src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java b/src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java new file mode 100644 index 000000000..973e972c4 --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 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.uri; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +/** + * Builder for {@link SchemaLoader}. + */ +public class SchemaLoaderBuilder { + private BiFunction, List, SchemaLoader> schemaLoaderFactory = DefaultSchemaLoader::new; + private List schemaLoaders = new ArrayList<>(); + private List absoluteIriMappers = new ArrayList<>(); + + public SchemaLoaderBuilder() { + this.schemaLoaders.add(new ClasspathSchemaLoader()); + this.schemaLoaders.add(new UriSchemaLoader()); + } + + public SchemaLoaderBuilder schemaLoaderFactory( + BiFunction, List, SchemaLoader> schemaLoaderFactory) { + this.schemaLoaderFactory = schemaLoaderFactory; + return this; + } + + public SchemaLoaderBuilder schemaLoaders(List schemaLoaders) { + this.schemaLoaders = schemaLoaders; + return this; + } + + public SchemaLoaderBuilder absoluteIriMappers(List absoluteIriMappers) { + this.absoluteIriMappers = absoluteIriMappers; + return this; + } + + public SchemaLoaderBuilder schemaLoaders(Consumer> schemaLoaderCustomizer) { + schemaLoaderCustomizer.accept(this.schemaLoaders); + return this; + } + + public SchemaLoaderBuilder absoluteIriMappers(Consumer> absoluteIriCustomizer) { + absoluteIriCustomizer.accept(this.absoluteIriMappers); + return this; + } + + public SchemaLoaderBuilder absoluteIriMapper(AbsoluteIriMapper absoluteIriMapper) { + this.absoluteIriMappers.add(absoluteIriMapper); + return this; + } + + public SchemaLoaderBuilder schemaLoader(SchemaLoader schemaLoader) { + this.schemaLoaders.add(0, schemaLoader); + return this; + } + + public SchemaLoaderBuilder mapPrefix(String source, String replacement) { + this.absoluteIriMappers.add(new PrefixAbsoluteIriMapper(source, replacement)); + return this; + } + + public SchemaLoaderBuilder map(Map mappings) { + this.absoluteIriMappers.add(new MapAbsoluteIriMapper(mappings)); + return this; + } + + public SchemaLoader build() { + return schemaLoaderFactory.apply(this.schemaLoaders, this.absoluteIriMappers); + } + +} diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java index f95be7dfa..3a5325857 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java @@ -184,10 +184,9 @@ private JsonSchemaFactory buildValidatorFactory(VersionFlag defaultVersion, Test return JsonSchemaFactory .builder(base) .objectMapper(this.mapper) - .absoluteIriMappers(absoluteIriMappers -> { - absoluteIriMappers.add(new PrefixAbsoluteIriMapper("https://", "http://")); - absoluteIriMappers.add(new PrefixAbsoluteIriMapper("http://json-schema.org", "resource:")); - }) + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder + .mapPrefix("https://", "http://") + .mapPrefix("http://json-schema.org", "resource:")) .build(); } diff --git a/src/test/java/com/networknt/schema/CustomUriTest.java b/src/test/java/com/networknt/schema/CustomUriTest.java index 3df482c01..b86a9dff4 100644 --- a/src/test/java/com/networknt/schema/CustomUriTest.java +++ b/src/test/java/com/networknt/schema/CustomUriTest.java @@ -31,7 +31,7 @@ public void customUri() throws Exception { private JsonSchemaFactory buildJsonSchemaFactory() { return JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)) - .schemaLoaders(schemaLoaders -> schemaLoaders.add(0, new CustomUriFetcher())).build(); + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.schemaLoader(new CustomUriFetcher())).build(); } private static class CustomUriFetcher implements SchemaLoader { diff --git a/src/test/java/com/networknt/schema/Issue285Test.java b/src/test/java/com/networknt/schema/Issue285Test.java index 2c1b6980b..9c7b33e69 100644 --- a/src/test/java/com/networknt/schema/Issue285Test.java +++ b/src/test/java/com/networknt/schema/Issue285Test.java @@ -18,11 +18,10 @@ public class Issue285Test { private JsonSchemaFactory schemaFactory = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)) .objectMapper(mapper) - .absoluteIriMappers(absoluteIriMappers -> { - absoluteIriMappers.add(new PrefixAbsoluteIriMapper("http://json-schema.org", "resource:")); - absoluteIriMappers.add(new PrefixAbsoluteIriMapper("https://json-schema.org", "resource:")); - }) - .build(); + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder + .mapPrefix("http://json-schema.org", "resource:") + .mapPrefix("https://json-schema.org", "resource:")) + .build(); String schemaStr = "{\n" + diff --git a/src/test/java/com/networknt/schema/Issue665Test.java b/src/test/java/com/networknt/schema/Issue665Test.java index 2e4f2d680..1d57e6f7d 100644 --- a/src/test/java/com/networknt/schema/Issue665Test.java +++ b/src/test/java/com/networknt/schema/Issue665Test.java @@ -3,8 +3,6 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import com.networknt.schema.uri.MapAbsoluteIriMapper; - import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -27,9 +25,9 @@ void testUrnUriAsLocalRef() throws IOException { void testUrnUriAsLocalRef_ExternalURN() { JsonSchemaFactory factory = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)) - .absoluteIriMappers(absoluteIriMappers -> { - absoluteIriMappers.add(new MapAbsoluteIriMapper(Collections.singletonMap("urn:data", - "classpath:draft7/urn/issue665_external_urn_subschema.json"))); + .schemaLoaderBuilder(schemaLoaderBuilder -> { + schemaLoaderBuilder.map(Collections.singletonMap("urn:data", + "classpath:draft7/urn/issue665_external_urn_subschema.json")); }) .build(); diff --git a/src/test/java/com/networknt/schema/Issue824Test.java b/src/test/java/com/networknt/schema/Issue824Test.java index eb483565b..bcb2229b4 100644 --- a/src/test/java/com/networknt/schema/Issue824Test.java +++ b/src/test/java/com/networknt/schema/Issue824Test.java @@ -9,15 +9,14 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.uri.PrefixAbsoluteIriMapper; public class Issue824Test { @Test void validate() throws JsonProcessingException { final JsonSchema v201909SpecSchema = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)) - .absoluteIriMappers(absoluteIriMappers -> { - absoluteIriMappers.add(new PrefixAbsoluteIriMapper("https://json-schema.org", "resource:")); + .schemaLoaderBuilder(schemaLoaderBuilder -> { + schemaLoaderBuilder.mapPrefix("https://json-schema.org", "resource:"); }).build() .getSchema(SchemaLocation.of(JsonMetaSchema.getV201909().getUri())); v201909SpecSchema.preloadJsonSchema(); diff --git a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java index 83aac6da4..71233d591 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java @@ -45,7 +45,7 @@ private void runCacheTest(boolean enableCache) throws JsonProcessingException { private JsonSchemaFactory buildJsonSchemaFactory(CustomURIFetcher uriFetcher, boolean enableUriSchemaCache) { return JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)) .enableUriSchemaCache(enableUriSchemaCache) - .schemaLoaders(schemaLoaders -> schemaLoaders.add(0, uriFetcher)) + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.schemaLoader(uriFetcher)) .addMetaSchema(JsonMetaSchema.getV202012()) .build(); } diff --git a/src/test/java/com/networknt/schema/UriMappingTest.java b/src/test/java/com/networknt/schema/UriMappingTest.java index 4eb37c049..13b843d14 100644 --- a/src/test/java/com/networknt/schema/UriMappingTest.java +++ b/src/test/java/com/networknt/schema/UriMappingTest.java @@ -49,7 +49,7 @@ public void testBuilderUriMappingUri() throws IOException { Builder builder = JsonSchemaFactory.builder() .defaultMetaSchemaURI(draftV4.getUri()) .addMetaSchema(draftV4) - .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(getUriMappingsFromUrl(mappings))); + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))); JsonSchemaFactory instance = builder.build(); JsonSchema schema = instance.getSchema(SchemaLocation.of( "https://raw.githubusercontent.com/networknt/json-schema-validator/master/src/test/resources/draft4/extra/uri_mapping/uri-mapping.schema.json")); @@ -87,7 +87,7 @@ public void testBuilderExampleMappings() throws IOException { Builder builder = JsonSchemaFactory.builder() .defaultMetaSchemaURI(draftV4.getUri()) .addMetaSchema(draftV4) - .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(getUriMappingsFromUrl(mappings))); + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))); instance = builder.build(); JsonSchema schema = instance.getSchema(example); assertEquals(0, schema.validate(mapper.createObjectNode()).size()); @@ -103,7 +103,7 @@ public void testBuilderExampleMappings() throws IOException { public void testValidatorConfigUriMappingUri() throws IOException { URL mappings = UriMappingTest.class.getResource("/draft4/extra/uri_mapping/uri-mapping.json"); JsonSchemaFactory instance = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) - .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(getUriMappingsFromUrl(mappings))).build(); + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))).build(); JsonSchema schema = instance.getSchema(SchemaLocation.of( "https://raw.githubusercontent.com/networknt/json-schema-validator/master/src/test/resources/draft4/extra/uri_mapping/uri-mapping.schema.json")); assertEquals(0, schema.validate(mapper.readTree(mappings)).size()); @@ -121,7 +121,7 @@ public void testValidatorConfigUriMappingUri() throws IOException { public void testValidatorConfigExampleMappings() throws IOException { URL mappings = UriMappingTest.class.getResource("/draft4/extra/uri_mapping/invalid-schema-uri.json"); JsonSchemaFactory instance = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) - .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(getUriMappingsFromUrl(mappings))).build(); + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))).build(); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); SchemaLocation example = SchemaLocation.of("http://example.com/invalid/schema/url"); // first test that attempting to use example URL throws an error @@ -146,7 +146,7 @@ public void testValidatorConfigExampleMappings() throws IOException { public void testMappingsForRef() throws IOException { URL mappings = UriMappingTest.class.getResource("/draft4/extra/uri_mapping/schema-with-ref-mapping.json"); JsonSchemaFactory instance = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) - .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(getUriMappingsFromUrl(mappings))).build(); + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))).build(); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); JsonSchema schema = instance.getSchema(SchemaLocation.of("resource:draft4/extra/uri_mapping/schema-with-ref.json"), config); diff --git a/src/test/java/com/networknt/schema/UrnTest.java b/src/test/java/com/networknt/schema/UrnTest.java index dc27aea52..02c4d03db 100644 --- a/src/test/java/com/networknt/schema/UrnTest.java +++ b/src/test/java/com/networknt/schema/UrnTest.java @@ -29,7 +29,7 @@ public void testURNToURI() throws Exception { JsonSchemaFactory.Builder builder = JsonSchemaFactory.builder() .defaultMetaSchemaURI(draftV7.getUri()) .addMetaSchema(draftV7) - .absoluteIriMappers(absoluteIriMappers -> absoluteIriMappers.add(value -> AbsoluteIri.of(String.format("resource:draft7/urn/%s.schema.json", value.toString()))) + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(value -> AbsoluteIri.of(String.format("resource:draft7/urn/%s.schema.json", value.toString()))) ); JsonSchemaFactory instance = builder.build(); JsonSchema schema = instance.getSchema(is); From db270485970a295ea57ecd6a9ee8ac2975fa0510 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 10 Jan 2024 16:21:25 +0800 Subject: [PATCH 05/65] Refactor --- .../networknt/schema/BaseJsonValidator.java | 1 - .../java/com/networknt/schema/JsonSchema.java | 41 ++++++++++++++++++- .../networknt/schema/JsonSchemaFactory.java | 11 ++--- .../com/networknt/schema/Issue928Test.java | 3 +- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 724e03d7d..9b48b7936 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -22,7 +22,6 @@ import org.slf4j.Logger; -import java.net.URI; import java.util.Collection; import java.util.Iterator; import java.util.Map; diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 9138c66fe..a3b7574ca 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -24,7 +24,6 @@ import com.networknt.schema.walk.WalkListenerRunner; import java.io.UnsupportedEncodingException; -import java.net.URI; import java.net.URLDecoder; import java.util.*; @@ -189,6 +188,46 @@ public JsonNode getRefSchemaNode(String ref) { return node; } + + public JsonSchema getSubSchema(JsonNodePath fragment) { + JsonSchema document = findSchemaResourceRoot(); + JsonSchema parent = document; + JsonSchema subSchema = null; + for (int x = 0; x < fragment.getNameCount(); x++) { + Object segment = fragment.getElement(x); + JsonNode subSchemaNode = parent.getNode(segment); + if (subSchemaNode != null) { + SchemaLocation schemaLocation = parent.getSchemaLocation(); + JsonNodePath evaluationPath = parent.getEvaluationPath(); + if (segment instanceof Number) { + int index = ((Number) segment).intValue(); + schemaLocation = schemaLocation.append(index); + evaluationPath = evaluationPath.append(index); + } else { + schemaLocation = schemaLocation.append(segment.toString()); + evaluationPath = evaluationPath.append(segment.toString()); + } + subSchema = parent.getValidationContext().newSchema(schemaLocation, evaluationPath, subSchemaNode, + parent); + parent = subSchema; + } else { + throw new JsonSchemaException("Unable to find subschema " + fragment.toString() + " in " + + document.getSchemaLocation().toString()); + } + } + return subSchema; + } + + protected JsonNode getNode(Object propertyOrIndex) { + JsonNode node = getSchemaNode(); + JsonNode value = null; + if (propertyOrIndex instanceof Number) { + value = node.get(((Number) propertyOrIndex).intValue()); + } else { + value = node.get(propertyOrIndex.toString()); + } + return value; + } public JsonSchema findLexicalRoot() { JsonSchema ancestor = this; diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index f8349965a..bcd0c7bad 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -290,7 +290,7 @@ public JsonSchema getSchema(final SchemaLocation schemaUri, final SchemaValidato protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValidatorsConfig config) { try (InputStream inputStream = this.schemaLoader.getSchema(schemaUri).getInputStream()) { if (inputStream == null) { - throw new IOException("Cannot load schema uri"); + throw new IOException("Cannot load schema at " + schemaUri.toString()); } final JsonNode schemaNode; if (isYaml(schemaUri)) { @@ -312,12 +312,7 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid final ValidationContext validationContext = createValidationContext(schemaNode, config); SchemaLocation documentLocation = new SchemaLocation(schemaLocation.getAbsoluteIri()); JsonSchema document = doCreate(validationContext, documentLocation, evaluationPath, schemaNode, null, false); - JsonNode subSchemaNode = document.getRefSchemaNode("#" + schemaLocation.getFragment().toString()); - if (subSchemaNode != null) { - jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, subSchemaNode, document, false); - } else { - throw new JsonSchemaException("Unable to find subschema"); - } + return document.getSubSchema(schemaLocation.getFragment()); } return jsonSchema; } catch (IOException e) { @@ -325,7 +320,7 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid throw new JsonSchemaException(e); } } - + public JsonSchema getSchema(final SchemaLocation schemaUri) { return getSchema(schemaUri, new SchemaValidatorsConfig()); } diff --git a/src/test/java/com/networknt/schema/Issue928Test.java b/src/test/java/com/networknt/schema/Issue928Test.java index 778c2af95..6e23d8822 100644 --- a/src/test/java/com/networknt/schema/Issue928Test.java +++ b/src/test/java/com/networknt/schema/Issue928Test.java @@ -1,7 +1,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.uri.PrefixAbsoluteIriMapper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -12,7 +11,7 @@ private JsonSchemaFactory factoryFor(SpecVersion.VersionFlag version) { return JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(version)) .objectMapper(mapper) - .absoluteIriMappers(mappers -> mappers.add(new PrefixAbsoluteIriMapper("https://example.org", "classpath:"))) + .schemaLoaderBuilder(builder -> builder.mapPrefix("https://example.org", "classpath:")) .build(); } From a206a856ec9aad36557288ec9a757b2f77cb7504 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 12 Jan 2024 08:32:24 +0800 Subject: [PATCH 06/65] Fix test --- src/test/java/com/networknt/schema/UriMappingTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/networknt/schema/UriMappingTest.java b/src/test/java/com/networknt/schema/UriMappingTest.java index 13b843d14..1d916c8d4 100644 --- a/src/test/java/com/networknt/schema/UriMappingTest.java +++ b/src/test/java/com/networknt/schema/UriMappingTest.java @@ -120,8 +120,8 @@ public void testValidatorConfigUriMappingUri() throws IOException { @Test public void testValidatorConfigExampleMappings() throws IOException { URL mappings = UriMappingTest.class.getResource("/draft4/extra/uri_mapping/invalid-schema-uri.json"); - JsonSchemaFactory instance = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))).build(); + JsonSchemaFactory instance = JsonSchemaFactory + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)).build(); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); SchemaLocation example = SchemaLocation.of("http://example.com/invalid/schema/url"); // first test that attempting to use example URL throws an error @@ -138,6 +138,8 @@ public void testValidatorConfigExampleMappings() throws IOException { } catch (Exception ex) { fail("Unexpected exception thrown"); } + instance = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))).build(); JsonSchema schema = instance.getSchema(example, config); assertEquals(0, schema.validate(mapper.createObjectNode()).size()); } From 59b0303bfe331700bb9588ba0e5a683cfa3ef786 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:44:29 +0800 Subject: [PATCH 07/65] Refactor --- src/main/java/com/networknt/schema/RefValidator.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 80aada7d1..2ddbf3a52 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -57,7 +57,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val SchemaLocation schemaLocation = SchemaLocation.of(schemaUriFinal); // This should retrieve schemas regardless of the protocol that is in the uri. return new JsonSchemaRef(new CachedSupplier<>(() -> { - JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaUriFinal.toString()); + JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaUriFinal); if (schemaResource == null) { schemaResource = validationContext.getJsonSchemaFactory().getSchema(schemaLocation, validationContext.getConfig()); if (schemaResource != null) { @@ -139,7 +139,6 @@ private static JsonSchema getJsonSchema(JsonNode node, JsonSchema parent, if (node != null) { SchemaLocation path = null; JsonSchema currentParent = parent; - SchemaLocation currentUri = parent.getSchemaLocation(); if (refValue.startsWith(REF_CURRENT)) { // relative to document path = new SchemaLocation(parent.schemaLocation.getAbsoluteIri(), @@ -153,12 +152,10 @@ private static JsonSchema getJsonSchema(JsonNode node, JsonSchema parent, if (id != null) { if (id.contains(":")) { // absolute - currentUri = currentUri.resolve(id); path = SchemaLocation.of(id); } else { // relative String absoluteUri = path.getAbsoluteIri().resolve(id).toString(); - currentUri = currentUri.resolve(absoluteUri); path = SchemaLocation.of(absoluteUri); } } From 73287c903bf0c6bb70e2b312b31e89fb3b5a0f01 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:58:34 +0800 Subject: [PATCH 08/65] Add test --- .../com/networknt/schema/JsonMetaSchema.java | 5 +- .../com/networknt/schema/RefValidator.java | 7 +- .../schema/uri/ClasspathSchemaLoader.java | 8 ++- .../java/com/networknt/schema/RefTest.java | 64 +++++++++++++++++++ .../schema/ref-main-schema-resource.json | 51 +++++++++++++++ src/test/resources/schema/ref-main.json | 18 ++++++ src/test/resources/schema/ref-ref.json | 24 +++++++ 7 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 src/test/java/com/networknt/schema/RefTest.java create mode 100644 src/test/resources/schema/ref-main-schema-resource.json create mode 100644 src/test/resources/schema/ref-main.json create mode 100644 src/test/resources/schema/ref-ref.json diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index 7c688a1aa..813501fb0 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -298,5 +298,8 @@ public JsonValidator newValidator(ValidationContext validationContext, SchemaLoc } } - + @Override + public String toString() { + return this.uri; + } } diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 2ddbf3a52..8050cf007 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -126,14 +126,13 @@ private static JsonSchema getJsonSchema(JsonSchema parent, JsonNode node = parent.getRefSchemaNode(refValue); if (node != null) { return validationContext.getSchemaReferences().computeIfAbsent(refValueOriginal, key -> { - return getJsonSchema(node, parent, validationContext, refValue, evaluationPath); + return getJsonSchema(node, parent, refValue, evaluationPath); }); } return null; } private static JsonSchema getJsonSchema(JsonNode node, JsonSchema parent, - ValidationContext validationContext, String refValue, JsonNodePath evaluationPath) { if (node != null) { @@ -148,7 +147,7 @@ private static JsonSchema getJsonSchema(JsonNode node, JsonSchema parent, if (refParts.length > 3) { String[] subschemaParts = Arrays.copyOf(refParts, refParts.length - 2); JsonNode subschemaNode = parent.getRefSchemaNode(String.join("/", subschemaParts)); - String id = validationContext.resolveSchemaId(subschemaNode); + String id = parent.getValidationContext().resolveSchemaId(subschemaNode); if (id != null) { if (id.contains(":")) { // absolute @@ -176,7 +175,7 @@ private static JsonSchema getJsonSchema(JsonNode node, JsonSchema parent, path = path.append(parts[x]); } } - return validationContext.newSchema(path, evaluationPath, node, currentParent); + return parent.getValidationContext().newSchema(path, evaluationPath, node, currentParent); } throw null; } diff --git a/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java b/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java index c1f49cb3b..d357ba29f 100644 --- a/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java +++ b/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java @@ -34,10 +34,14 @@ public InputStreamSource getSchema(SchemaLocation schemaLocation) { } ClassLoader loader = classLoader; String name = schemaLocation.getAbsoluteIri().toString().substring(scheme.length() + 1); + if (name.startsWith("//")) { + name = name.substring(2); + } + String resource = name; return () -> { - InputStream result = loader.getResourceAsStream(name); + InputStream result = loader.getResourceAsStream(resource); if (result == null) { - result = loader.getResourceAsStream(name.substring(1)); + result = loader.getResourceAsStream(resource.substring(1)); } return result; }; diff --git a/src/test/java/com/networknt/schema/RefTest.java b/src/test/java/com/networknt/schema/RefTest.java new file mode 100644 index 000000000..1f02d7b17 --- /dev/null +++ b/src/test/java/com/networknt/schema/RefTest.java @@ -0,0 +1,64 @@ +package com.networknt.schema; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; + +public class RefTest { + private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder().build(); + + @Test + void shouldLoadRelativeClasspathReference() throws JsonMappingException, JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(SchemaLocation.of("classpath:///schema/ref-main.json"), config); + String input = "{\r\n" + + " \"DriverProperties\": {\r\n" + + " \"CommonProperties\": {\r\n" + + " \"field2\": \"abc-def-xyz\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + assertEquals("https://json-schema.org/draft-04/schema", schema.getValidationContext().getMetaSchema().getUri()); + Set errors = schema.validate(OBJECT_MAPPER.readTree(input)); + assertEquals(1, errors.size()); + ValidationMessage error = errors.iterator().next(); + assertEquals("classpath:///schema/ref-ref.json#/definitions/DriverProperties/required", + error.getSchemaLocation().toString()); + assertEquals("/properties/DriverProperties/properties/CommonProperties/$ref/required", + error.getEvaluationPath().toString()); + assertEquals("field1", error.getProperty()); + } + + @Test + void shouldLoadSchemaResource() throws JsonMappingException, JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(SchemaLocation.of("classpath:///schema/ref-main-schema-resource.json"), config); + String input = "{\r\n" + + " \"DriverProperties\": {\r\n" + + " \"CommonProperties\": {\r\n" + + " \"field2\": \"abc-def-xyz\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + assertEquals("https://json-schema.org/draft-04/schema", schema.getValidationContext().getMetaSchema().getUri()); + Set errors = schema.validate(OBJECT_MAPPER.readTree(input)); + assertEquals(1, errors.size()); + ValidationMessage error = errors.iterator().next(); + assertEquals("https://www.example.org/common#/definitions/DriverProperties/required", + error.getSchemaLocation().toString()); + assertEquals("/properties/DriverProperties/properties/CommonProperties/$ref/required", + error.getEvaluationPath().toString()); + assertEquals("field1", error.getProperty()); + } +} diff --git a/src/test/resources/schema/ref-main-schema-resource.json b/src/test/resources/schema/ref-main-schema-resource.json new file mode 100644 index 000000000..5794df791 --- /dev/null +++ b/src/test/resources/schema/ref-main-schema-resource.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://www.example.org/driver", + "type": "object", + "required": [ + "DriverProperties" + ], + "properties": { + "DriverProperties": { + "type": "object", + "properties": { + "CommonProperties": { + "$ref": "common#/definitions/DriverProperties" + } + }, + "required": [ + "CommonProperties" + ], + "additionalProperties": false + } + }, + "additionalProperties": false, + "definitions": { + "common": { + "id": "https://www.example.org/common", + "type": "object", + "additionalProperties": false, + "definitions": { + "DriverProperties": { + "type": "object", + "properties": { + "field1": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "field2": { + "type": "string", + "minLength": 1, + "maxLength": 512 + } + }, + "required": [ + "field1" + ], + "additionalProperties": false + } + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/schema/ref-main.json b/src/test/resources/schema/ref-main.json new file mode 100644 index 000000000..c219fec5f --- /dev/null +++ b/src/test/resources/schema/ref-main.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required" : ["DriverProperties"], + "properties": { + "DriverProperties": { + "type": "object", + "properties": { + "CommonProperties": { + "$ref": "ref-ref.json#/definitions/DriverProperties" + } + }, + "required": ["CommonProperties"], + "additionalProperties": false + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/src/test/resources/schema/ref-ref.json b/src/test/resources/schema/ref-ref.json new file mode 100644 index 000000000..3b43a5d06 --- /dev/null +++ b/src/test/resources/schema/ref-ref.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "definitions": { + "DriverProperties": { + "type": "object", + "properties": { + "field1": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "field2": { + "type": "string", + "minLength": 1, + "maxLength": 512 + } + }, + "required": ["field1"], + "additionalProperties": false + } + } +} \ No newline at end of file From 97bfdce9c849663419613336441a9357fc428dc6 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 19 Jan 2024 21:31:04 +0800 Subject: [PATCH 09/65] Ensure correct meta schema --- .../networknt/schema/JsonSchemaFactory.java | 21 ++++++++++++++++++- .../java/com/networknt/schema/RefTest.java | 5 +++++ .../schema/ref-main-schema-resource.json | 3 ++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index bcd0c7bad..474695e88 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -206,7 +206,18 @@ public JsonSchema create(ValidationContext validationContext, SchemaLocation sch } private JsonSchema doCreate(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, boolean suppressSubSchemaRetrieval) { - return JsonSchema.from(validationContext, schemaLocation, evaluationPath, schemaNode, parentSchema, suppressSubSchemaRetrieval); + return JsonSchema.from(withMetaSchema(validationContext, schemaNode), schemaLocation, evaluationPath, + schemaNode, parentSchema, suppressSubSchemaRetrieval); + } + + private ValidationContext withMetaSchema(ValidationContext validationContext, JsonNode schemaNode) { + JsonMetaSchema metaSchema = getMetaSchema(schemaNode); + if (metaSchema != null && !metaSchema.getUri().equals(validationContext.getMetaSchema().getUri())) { + return new ValidationContext(metaSchema, validationContext.getJsonSchemaFactory(), + validationContext.getConfig(), validationContext.getSchemaReferences(), + validationContext.getSchemaResources()); + } + return validationContext; } /** @@ -230,6 +241,14 @@ protected ValidationContext createValidationContext(final JsonNode schemaNode, S final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); return new ValidationContext(jsonMetaSchema, this, config); } + + private JsonMetaSchema getMetaSchema(final JsonNode schemaNode) { + final JsonNode uriNode = schemaNode.get("$schema"); + if (uriNode != null && uriNode.isTextual()) { + return jsonMetaSchemas.computeIfAbsent(normalizeMetaSchemaUri(uriNode.textValue()), this::fromId); + } + return null; + } private JsonMetaSchema findMetaSchemaForSchema(final JsonNode schemaNode) { final JsonNode uriNode = schemaNode.get("$schema"); diff --git a/src/test/java/com/networknt/schema/RefTest.java b/src/test/java/com/networknt/schema/RefTest.java index 1f02d7b17..c628b3597 100644 --- a/src/test/java/com/networknt/schema/RefTest.java +++ b/src/test/java/com/networknt/schema/RefTest.java @@ -60,5 +60,10 @@ void shouldLoadSchemaResource() throws JsonMappingException, JsonProcessingExcep assertEquals("/properties/DriverProperties/properties/CommonProperties/$ref/required", error.getEvaluationPath().toString()); assertEquals("field1", error.getProperty()); + JsonSchema driver = schema.getValidationContext().getSchemaResources().get("https://www.example.org/driver#"); + JsonSchema common = schema.getValidationContext().getSchemaResources().get("https://www.example.org/common#"); + assertEquals("https://json-schema.org/draft-04/schema", driver.getValidationContext().getMetaSchema().getUri()); + assertEquals("https://json-schema.org/draft-07/schema", common.getValidationContext().getMetaSchema().getUri()); + } } diff --git a/src/test/resources/schema/ref-main-schema-resource.json b/src/test/resources/schema/ref-main-schema-resource.json index 5794df791..c42fecde4 100644 --- a/src/test/resources/schema/ref-main-schema-resource.json +++ b/src/test/resources/schema/ref-main-schema-resource.json @@ -22,7 +22,8 @@ "additionalProperties": false, "definitions": { "common": { - "id": "https://www.example.org/common", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://www.example.org/common", "type": "object", "additionalProperties": false, "definitions": { From 52e0e459b3fd1352fcc1668418e953b1dff145aa Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 09:04:10 +0800 Subject: [PATCH 10/65] Support dynamic ref --- .../networknt/schema/DynamicRefValidator.java | 165 ++++++++++++++++++ .../com/networknt/schema/JsonMetaSchema.java | 8 + .../java/com/networknt/schema/JsonSchema.java | 40 ++++- .../networknt/schema/JsonSchemaFactory.java | 4 +- .../networknt/schema/ValidationContext.java | 15 +- .../networknt/schema/ValidatorTypeCode.java | 1 + .../com/networknt/schema/Version202012.java | 1 + 7 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/networknt/schema/DynamicRefValidator.java diff --git a/src/main/java/com/networknt/schema/DynamicRefValidator.java b/src/main/java/com/networknt/schema/DynamicRefValidator.java new file mode 100644 index 000000000..2efbbbf78 --- /dev/null +++ b/src/main/java/com/networknt/schema/DynamicRefValidator.java @@ -0,0 +1,165 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.CollectorContext.Scope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +/** + * Resolves $dynamicRef. + */ +public class DynamicRefValidator extends BaseJsonValidator { + private static final Logger logger = LoggerFactory.getLogger(DynamicRefValidator.class); + + protected JsonSchemaRef schema; + + public DynamicRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.REF, validationContext); + String refValue = schemaNode.asText(); + this.schema = getRefSchema(parentSchema, validationContext, refValue, evaluationPath); + } + + static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue, + JsonNodePath evaluationPath) { + String ref = resolve(parentSchema, refValue); + return new JsonSchemaRef(new CachedSupplier<>(() -> { + JsonSchema refSchema = validationContext.getDynamicAnchors().get(ref); + if (refSchema == null) { // This is a $dynamicRef without a matching $dynamicAnchor + // A $dynamicRef without a matching $dynamicAnchor in the same schema resource + // behaves like a normal $ref to $anchor + // A $dynamicRef without anchor in fragment behaves identical to $ref + JsonSchemaRef r = RefValidator.getRefSchema(parentSchema, validationContext, ref, evaluationPath); + if (r != null) { + refSchema = r.getSchema(); + } + } else { + // Check parents + JsonSchema base = parentSchema; + int index = ref.indexOf("#"); + String anchor = ref.substring(index); + String absoluteIri = ref.substring(0, index); + while (base.getEvaluationParentSchema() != null) { + base = base.getEvaluationParentSchema(); + if (!base.getSchemaLocation().getAbsoluteIri().toString().equals(absoluteIri)) { + absoluteIri = base.getSchemaLocation().getAbsoluteIri().toString(); + String parentRef = SchemaLocation.resolve(base.getSchemaLocation(), anchor); + JsonSchema parentRefSchema = validationContext.getDynamicAnchors().get(parentRef); + if (parentRefSchema != null) { + refSchema = parentRefSchema; + } + } + } + } + + if (refSchema != null) { + refSchema = refSchema.fromRef(parentSchema, evaluationPath); + } + return refSchema; + })); + } + + private static String resolve(JsonSchema parentSchema, String refValue) { + // $ref prevents a sibling $id from changing the base uri + JsonSchema base = parentSchema; + if (parentSchema.getId() != null && parentSchema.parentSchema != null) { + base = parentSchema.parentSchema; + } + return SchemaLocation.resolve(base.getSchemaLocation(), refValue); + } + + + @Override + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + CollectorContext collectorContext = executionContext.getCollectorContext(); + + Set errors = Collections.emptySet(); + + Scope parentScope = collectorContext.enterDynamicScope(); + try { + debug(logger, node, rootNode, instanceLocation); + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) + .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") + .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) + .arguments(schemaNode.asText()).build(); + throw new JsonSchemaException(validationMessage); + } + errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); + } finally { + Scope scope = collectorContext.exitDynamicScope(); + if (errors.isEmpty()) { + parentScope.mergeWith(scope); + } + } + return errors; + } + + @Override + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { + CollectorContext collectorContext = executionContext.getCollectorContext(); + + Set errors = Collections.emptySet(); + + Scope parentScope = collectorContext.enterDynamicScope(); + try { + debug(logger, node, rootNode, instanceLocation); + // 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. + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) + .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") + .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) + .arguments(schemaNode.asText()).build(); + throw new JsonSchemaException(validationMessage); + } + errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + return errors; + } finally { + Scope scope = collectorContext.exitDynamicScope(); + if (shouldValidateSchema) { + if (errors.isEmpty()) { + parentScope.mergeWith(scope); + } + } + } + } + + public JsonSchemaRef getSchemaRef() { + return this.schema; + } + + + @Override + public void preloadJsonSchema() { + JsonSchema jsonSchema = null; + try { + jsonSchema = this.schema.getSchema(); + } catch (JsonSchemaException e) { + throw e; + } catch (RuntimeException e) { + throw new JsonSchemaException(e); + } + jsonSchema.initializeValidators(); + } +} diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index 813501fb0..dfeed1fd7 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -234,6 +234,14 @@ public String readAnchor(JsonNode schemaNode) { return null; } + public String readDynamicAnchor(JsonNode schemaNode) { + boolean supportsDynamicAnchor = this.keywords.containsKey("$dynamicAnchor"); + if (supportsDynamicAnchor) { + return readText(schemaNode, "$dynamicAnchor"); + } + return null; + } + public JsonNode getNodeByFragmentRef(String ref, JsonNode node) { boolean supportsAnchor = this.keywords.containsKey("$anchor"); String refName = supportsAnchor ? ref.substring(1) : ref; diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index a3b7574ca..e27441219 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -59,11 +59,10 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { super(schemaLocation.resolve(validationContext.resolveSchemaId(schemaNode)), evaluationPath, schemaNode, parent, null, null, validationContext, suppressSubSchemaRetrieval); - this.validationContext = validationContext; - this.metaSchema = validationContext.getMetaSchema(); + this.metaSchema = this.validationContext.getMetaSchema(); initializeConfig(); - this.id = validationContext.resolveSchemaId(this.schemaNode); - this.anchor = validationContext.getMetaSchema().readAnchor(this.schemaNode); + this.id = this.validationContext.resolveSchemaId(this.schemaNode); + this.anchor = this.validationContext.getMetaSchema().readAnchor(this.schemaNode); if (this.id != null) { this.validationContext.getSchemaResources() .putIfAbsent(this.schemaLocation != null ? this.schemaLocation.toString() : this.id, this); @@ -72,6 +71,11 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc this.validationContext.getSchemaResources() .putIfAbsent(this.schemaLocation.getAbsoluteIri().toString() + "#" + anchor, this); } + String dynamicAnchor = this.validationContext.getMetaSchema().readDynamicAnchor(schemaNode); + if (dynamicAnchor != null) { + this.validationContext.getDynamicAnchors() + .putIfAbsent(this.schemaLocation.getAbsoluteIri().toString() + "#" + dynamicAnchor, this); + } getValidators(); } @@ -116,7 +120,9 @@ public JsonSchema fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath ref copy.validationContext = new ValidationContext(copy.getValidationContext().getMetaSchema(), copy.getValidationContext().getJsonSchemaFactory(), refEvaluationParentSchema.validationContext.getConfig(), - copy.getValidationContext().getSchemaReferences(), copy.getValidationContext().getSchemaResources()); + refEvaluationParentSchema.getValidationContext().getSchemaReferences(), + refEvaluationParentSchema.getValidationContext().getSchemaResources(), + refEvaluationParentSchema.getValidationContext().getDynamicAnchors()); copy.evaluationPath = refEvaluationPath; copy.evaluationParentSchema = refEvaluationParentSchema; // Validator state is reset due to the changes in evaluation path @@ -134,7 +140,8 @@ public JsonSchema withConfig(SchemaValidatorsConfig config) { copy.validationContext = new ValidationContext(copy.getValidationContext().getMetaSchema(), copy.getValidationContext().getJsonSchemaFactory(), config, copy.getValidationContext().getSchemaReferences(), - copy.getValidationContext().getSchemaResources()); + copy.getValidationContext().getSchemaResources(), + copy.getValidationContext().getDynamicAnchors()); copy.validatorsLoaded = false; copy.requiredValidator = null; copy.typeValidator = null; @@ -188,7 +195,26 @@ public JsonNode getRefSchemaNode(String ref) { return node; } - + + public JsonSchema getRefSchema(JsonNodePath fragment) { + if (PathType.JSON_POINTER.equals(fragment.getPathType())) { + // Json Pointer + return getSubSchema(fragment); + } else { + // Anchor + String base = this.getSchemaLocation().getAbsoluteIri() != null ? this.schemaLocation.getAbsoluteIri().toString() : ""; + String anchor = base + "#" + fragment.toString(); + JsonSchema result = this.validationContext.getSchemaResources().get(anchor); + if (result == null) { + result = this.validationContext.getDynamicAnchors().get(anchor); + } + if (result == null) { + throw new JsonSchemaException("Unable to find anchor "+anchor); + } + return result; + } + } + public JsonSchema getSubSchema(JsonNodePath fragment) { JsonSchema document = findSchemaResourceRoot(); JsonSchema parent = document; diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 474695e88..a7e44b7cf 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -215,7 +215,7 @@ private ValidationContext withMetaSchema(ValidationContext validationContext, Js if (metaSchema != null && !metaSchema.getUri().equals(validationContext.getMetaSchema().getUri())) { return new ValidationContext(metaSchema, validationContext.getJsonSchemaFactory(), validationContext.getConfig(), validationContext.getSchemaReferences(), - validationContext.getSchemaResources()); + validationContext.getSchemaResources(), validationContext.getDynamicAnchors()); } return validationContext; } @@ -331,7 +331,7 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid final ValidationContext validationContext = createValidationContext(schemaNode, config); SchemaLocation documentLocation = new SchemaLocation(schemaLocation.getAbsoluteIri()); JsonSchema document = doCreate(validationContext, documentLocation, evaluationPath, schemaNode, null, false); - return document.getSubSchema(schemaLocation.getFragment()); + return document.getRefSchema(schemaLocation.getFragment()); } return jsonSchema; } catch (IOException e) { diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java index 31284fcb6..f0758e43c 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -29,15 +29,16 @@ public class ValidationContext { private final SchemaValidatorsConfig config; private final ConcurrentMap schemaReferences; private final ConcurrentMap schemaResources; + private final ConcurrentMap dynamicAnchors; public ValidationContext(JsonMetaSchema metaSchema, JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config) { - this(metaSchema, jsonSchemaFactory, config, new ConcurrentHashMap<>(), new ConcurrentHashMap<>()); + this(metaSchema, jsonSchemaFactory, config, new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), new ConcurrentHashMap<>()); } public ValidationContext(JsonMetaSchema metaSchema, JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config, ConcurrentMap schemaReferences, - ConcurrentMap schemaResources) { + ConcurrentMap schemaResources, ConcurrentMap dynamicAnchors) { if (metaSchema == null) { throw new IllegalArgumentException("JsonMetaSchema must not be null"); } @@ -49,6 +50,7 @@ public ValidationContext(JsonMetaSchema metaSchema, JsonSchemaFactory jsonSchema this.config = config == null ? new SchemaValidatorsConfig() : config; this.schemaReferences = schemaReferences; this.schemaResources = schemaResources; + this.dynamicAnchors = dynamicAnchors; } public JsonSchema newSchema(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema) { @@ -90,6 +92,15 @@ public ConcurrentMap getSchemaResources() { return this.schemaResources; } + /** + * Gets the dynamic anchors. + * + * @return the dynamic anchors + */ + public ConcurrentMap getDynamicAnchors() { + return this.dynamicAnchors; + } + public JsonMetaSchema getMetaSchema() { return this.metaSchema; } diff --git a/src/main/java/com/networknt/schema/ValidatorTypeCode.java b/src/main/java/com/networknt/schema/ValidatorTypeCode.java index 786b8ed8c..98f60bd4d 100644 --- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java +++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java @@ -65,6 +65,7 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType { DEPENDENCIES("dependencies", "1007", DependenciesValidator::new, VersionCode.AllVersions), DEPENDENT_REQUIRED("dependentRequired", "1045", DependentRequired::new, VersionCode.MinV201909), DEPENDENT_SCHEMAS("dependentSchemas", "1046", DependentSchemas::new, VersionCode.MinV201909), + DYNAMIC_REF("$dynamicRef", "1051", DynamicRefValidator::new, VersionCode.MinV202012), EDITS("edits", "1005", null, VersionCode.AllVersions), ENUM("enum", "1008", EnumValidator::new, VersionCode.AllVersions), EXCLUSIVE_MAXIMUM("exclusiveMaximum", "1038", ExclusiveMaximumValidator::new, VersionCode.MinV6), diff --git a/src/main/java/com/networknt/schema/Version202012.java b/src/main/java/com/networknt/schema/Version202012.java index f3a6da4cb..3d07de784 100644 --- a/src/main/java/com/networknt/schema/Version202012.java +++ b/src/main/java/com/networknt/schema/Version202012.java @@ -28,6 +28,7 @@ public JsonMetaSchema getInstance() { new NonValidationKeyword("$comment"), new NonValidationKeyword("$defs"), new NonValidationKeyword("$anchor"), + new NonValidationKeyword("$dynamicAnchor"), new NonValidationKeyword("deprecated"), new NonValidationKeyword("contentMediaType"), new NonValidationKeyword("contentEncoding"), From 1259752c443860c80867058995ddc33decacf338 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 11:03:08 +0800 Subject: [PATCH 11/65] Fix anchor --- .../com/networknt/schema/RefValidator.java | 34 +++++++++++++++++-- .../schema/JsonSchemaTestSuiteTest.java | 6 ---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 8050cf007..e7842faea 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -69,6 +69,10 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val validationContext.getSchemaReferences() .putAll(schemaResource.getValidationContext().getSchemaReferences()); } + if (!schemaResource.getValidationContext().getDynamicAnchors().isEmpty()) { + validationContext.getDynamicAnchors() + .putAll(schemaResource.getValidationContext().getDynamicAnchors()); + } } } if (index < 0) { @@ -78,8 +82,31 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val return schemaResource.fromRef(parentSchema, evaluationPath); } else { String newRefValue = refValue.substring(index); - schemaResource = getJsonSchema(schemaResource, validationContext, newRefValue, refValueOriginal, - evaluationPath); + String find = schemaLocation.getAbsoluteIri() + newRefValue; + JsonSchema findSchemaResource = validationContext.getSchemaResources().get(find); + if (schemaResource == null) { + schemaResource = validationContext.getJsonSchemaFactory().getSchema(schemaLocation, validationContext.getConfig()); + if (schemaResource != null) { + if (!schemaResource.getValidationContext().getSchemaResources().isEmpty()) { + validationContext.getSchemaResources() + .putAll(schemaResource.getValidationContext().getSchemaResources()); + } + if (!schemaResource.getValidationContext().getSchemaReferences().isEmpty()) { + validationContext.getSchemaReferences() + .putAll(schemaResource.getValidationContext().getSchemaReferences()); + } + if (!schemaResource.getValidationContext().getDynamicAnchors().isEmpty()) { + validationContext.getDynamicAnchors() + .putAll(schemaResource.getValidationContext().getDynamicAnchors()); + } + } + } + if (findSchemaResource != null) { + schemaResource = findSchemaResource; + } else { + schemaResource = getJsonSchema(schemaResource, validationContext, newRefValue, refValueOriginal, + evaluationPath); + } if (schemaResource == null) { return null; } @@ -92,6 +119,9 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val // Schema resource needs to update the parent and evaluation path return new JsonSchemaRef(new CachedSupplier<>(() -> { JsonSchema schemaResource = validationContext.getSchemaResources().get(absoluteIri); + if (schemaResource == null) { + schemaResource = validationContext.getDynamicAnchors().get(absoluteIri); + } if (schemaResource == null) { schemaResource = getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath); } diff --git a/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java b/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java index 034cc1a8b..0c09ff168 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java @@ -69,17 +69,11 @@ protected Optional reason(Path path) { } private void disableV202012Tests() { - this.disabled.put(Paths.get("src/test/suite/tests/draft2020-12/anchor.json"), "Unsupported behavior"); - this.disabled.put(Paths.get("src/test/suite/tests/draft2020-12/defs.json"), "Unsupported behavior"); - this.disabled.put(Paths.get("src/test/suite/tests/draft2020-12/dynamicRef.json"), "Unsupported behavior"); - this.disabled.put(Paths.get("src/test/suite/tests/draft2020-12/id.json"), "Unsupported behavior"); this.disabled.put(Paths.get("src/test/suite/tests/draft2020-12/optional/format-assertion.json"), "Unsupported behavior"); this.disabled.put(Paths.get("src/test/suite/tests/draft2020-12/vocabulary.json"), "Unsupported behavior"); } private void disableV201909Tests() { - this.disabled.put(Paths.get("src/test/suite/tests/draft2019-09/anchor.json"), "Unsupported behavior"); - this.disabled.put(Paths.get("src/test/suite/tests/draft2019-09/id.json"), "Unsupported behavior"); this.disabled.put(Paths.get("src/test/suite/tests/draft2019-09/vocabulary.json"), "Unsupported behavior"); } From 0956bbd29e2e761ede9a74287f08f1b426276bda Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:56:15 +0800 Subject: [PATCH 12/65] Refactor --- .../networknt/schema/ExecutionCustomizer.java | 2 +- .../networknt/schema/JsonSchemaFactory.java | 23 +++++++--- .../com/networknt/schema/RefValidator.java | 46 ++++++++----------- 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/networknt/schema/ExecutionCustomizer.java b/src/main/java/com/networknt/schema/ExecutionCustomizer.java index 7ae5d1a56..c24038b8d 100644 --- a/src/main/java/com/networknt/schema/ExecutionCustomizer.java +++ b/src/main/java/com/networknt/schema/ExecutionCustomizer.java @@ -20,7 +20,7 @@ * Customize the execution context before validation. */ @FunctionalInterface -interface ExecutionCustomizer { +public interface ExecutionCustomizer { /** * Customize the execution context before validation. *

diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index a7e44b7cf..d7a3f6c4b 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -339,7 +339,19 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid throw new JsonSchemaException(e); } } - + + public JsonSchema getSchema(final URI schemaUri) { + return getSchema(SchemaLocation.of(schemaUri.toString()), new SchemaValidatorsConfig()); + } + + public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode, final SchemaValidatorsConfig config) { + return newJsonSchema(SchemaLocation.of(schemaUri.toString()), jsonNode, config); + } + + public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode) { + return newJsonSchema(SchemaLocation.of(schemaUri.toString()), jsonNode, null); + } + public JsonSchema getSchema(final SchemaLocation schemaUri) { return getSchema(schemaUri, new SchemaValidatorsConfig()); } @@ -347,16 +359,15 @@ public JsonSchema getSchema(final SchemaLocation schemaUri) { public JsonSchema getSchema(final SchemaLocation schemaUri, final JsonNode jsonNode, final SchemaValidatorsConfig config) { return newJsonSchema(schemaUri, jsonNode, config); } - + + public JsonSchema getSchema(final SchemaLocation schemaUri, final JsonNode jsonNode) { + return newJsonSchema(schemaUri, jsonNode, null); + } public JsonSchema getSchema(final JsonNode jsonNode, final SchemaValidatorsConfig config) { return newJsonSchema(null, jsonNode, config); } - public JsonSchema getSchema(final SchemaLocation schemaUri, final JsonNode jsonNode) { - return newJsonSchema(schemaUri, jsonNode, null); - } - public JsonSchema getSchema(final JsonNode jsonNode) { return newJsonSchema(null, jsonNode, null); } diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index e7842faea..17ef67b2e 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -61,18 +61,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val if (schemaResource == null) { schemaResource = validationContext.getJsonSchemaFactory().getSchema(schemaLocation, validationContext.getConfig()); if (schemaResource != null) { - if (!schemaResource.getValidationContext().getSchemaResources().isEmpty()) { - validationContext.getSchemaResources() - .putAll(schemaResource.getValidationContext().getSchemaResources()); - } - if (!schemaResource.getValidationContext().getSchemaReferences().isEmpty()) { - validationContext.getSchemaReferences() - .putAll(schemaResource.getValidationContext().getSchemaReferences()); - } - if (!schemaResource.getValidationContext().getDynamicAnchors().isEmpty()) { - validationContext.getDynamicAnchors() - .putAll(schemaResource.getValidationContext().getDynamicAnchors()); - } + copySchemaResources(validationContext, schemaResource); } } if (index < 0) { @@ -84,22 +73,8 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val String newRefValue = refValue.substring(index); String find = schemaLocation.getAbsoluteIri() + newRefValue; JsonSchema findSchemaResource = validationContext.getSchemaResources().get(find); - if (schemaResource == null) { - schemaResource = validationContext.getJsonSchemaFactory().getSchema(schemaLocation, validationContext.getConfig()); - if (schemaResource != null) { - if (!schemaResource.getValidationContext().getSchemaResources().isEmpty()) { - validationContext.getSchemaResources() - .putAll(schemaResource.getValidationContext().getSchemaResources()); - } - if (!schemaResource.getValidationContext().getSchemaReferences().isEmpty()) { - validationContext.getSchemaReferences() - .putAll(schemaResource.getValidationContext().getSchemaReferences()); - } - if (!schemaResource.getValidationContext().getDynamicAnchors().isEmpty()) { - validationContext.getDynamicAnchors() - .putAll(schemaResource.getValidationContext().getDynamicAnchors()); - } - } + if (findSchemaResource == null) { + findSchemaResource = validationContext.getDynamicAnchors().get(find); } if (findSchemaResource != null) { schemaResource = findSchemaResource; @@ -138,6 +113,21 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val return new JsonSchemaRef(new CachedSupplier<>( () -> getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath))); } + + private static void copySchemaResources(ValidationContext validationContext, JsonSchema schemaResource) { + if (!schemaResource.getValidationContext().getSchemaResources().isEmpty()) { + validationContext.getSchemaResources() + .putAll(schemaResource.getValidationContext().getSchemaResources()); + } + if (!schemaResource.getValidationContext().getSchemaReferences().isEmpty()) { + validationContext.getSchemaReferences() + .putAll(schemaResource.getValidationContext().getSchemaReferences()); + } + if (!schemaResource.getValidationContext().getDynamicAnchors().isEmpty()) { + validationContext.getDynamicAnchors() + .putAll(schemaResource.getValidationContext().getDynamicAnchors()); + } + } private static String resolve(JsonSchema parentSchema, String refValue) { // $ref prevents a sibling $id from changing the base uri From 0cfa6c59c7472a6616e26665b8aa5a47064c23b3 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:56:33 +0800 Subject: [PATCH 13/65] Update docs --- README.md | 4 +- doc/compatibility.md | 10 +- doc/config.md | 8 - doc/cust-fetcher.md | 61 -------- doc/multiple-language.md | 10 +- doc/quickstart.md | 2 +- doc/schema-map.md | 141 ------------------ doc/schema-retrieval.md | 99 ++++++++++++ .../com/networknt/schema/CustomUriTest.java | 10 +- .../suite/tests/draft2019-09/refRemote.json | 4 - .../suite/tests/draft2020-12/refRemote.json | 3 - src/test/suite/tests/draft4/id.json | 8 +- 12 files changed, 122 insertions(+), 238 deletions(-) delete mode 100644 doc/cust-fetcher.md delete mode 100644 doc/schema-map.md create mode 100644 doc/schema-retrieval.md diff --git a/README.md b/README.md index 208147f2b..99af87497 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,7 @@ For the latest version, please check the [release](https://github.com/networknt/ ## [YAML Validation](doc/yaml.md) -## [Schema Mapping](doc/schema-map.md) - -## [Customized URIFetcher](doc/cust-fetcher.md) +## [Customizing Schema Retrieval](doc/schema-retrieval.md) ## [Customized MetaSchema](doc/cust-meta.md) diff --git a/doc/compatibility.md b/doc/compatibility.md index 292d8073f..525a02a01 100644 --- a/doc/compatibility.md +++ b/doc/compatibility.md @@ -12,13 +12,13 @@ | Keyword | Draft 4 | Draft 6 | Draft 7 | Draft 2019-09 | Draft 2020-12 | |:---------------------------|:-------:|:-------:|:-------:|:-------------:|:-------------:| -| $anchor | 🚫 | 🚫 | 🚫 | 🔴 | 🔴 | -| $dynamicAnchor | 🚫 | 🚫 | 🚫 | 🚫 | 🔴 | -| $dynamicRef | 🚫 | 🚫 | 🚫 | 🚫 | 🔴 | -| $id | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | +| $anchor | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| $dynamicAnchor | 🚫 | 🚫 | 🚫 | 🚫 | 🟢 | +| $dynamicRef | 🚫 | 🚫 | 🚫 | 🚫 | 🟢 | +| $id | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | | $recursiveAnchor | 🚫 | 🚫 | 🚫 | 🟢 | 🚫 | | $recursiveRef | 🚫 | 🚫 | 🚫 | 🟢 | 🚫 | -| $ref | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | +| $ref | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | | $vocabulary | 🚫 | 🚫 | 🚫 | 🔴 | 🔴 | | additionalItems | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | | additionalProperties | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | diff --git a/doc/config.md b/doc/config.md index 023c344c5..fb265d88e 100644 --- a/doc/config.md +++ b/doc/config.md @@ -45,14 +45,6 @@ The default value is true in the SchemaValidatorsConfig object. For more details, please refer to this [issue](https://github.com/networknt/json-schema-validator/issues/183). - -* uriMappings - -Map of public, typically internet-accessible schema URLs to alternate locations; this allows for offline validation of schemas that refer to public URLs. This is merged with any mappings the sonSchemaFactory -may have been built. - -The type for this variable is `Map`. - * javaSemantics When set to true, use Java-specific semantics rather than native JavaScript semantics. diff --git a/doc/cust-fetcher.md b/doc/cust-fetcher.md deleted file mode 100644 index 2b5c8a14e..000000000 --- a/doc/cust-fetcher.md +++ /dev/null @@ -1,61 +0,0 @@ -# Custom URIFetcher - -The default `URIFetcher` implementation uses JDK connection/socket without handling network exceptions. It works in most of the cases; however, if you want to have a customized implementation, you can do so. One user has his implementation with urirest to handle the timeout. A detailed discussion can be found in this [issue](https://github.com/networknt/json-schema-validator/issues/240) - -## Example implementation - -The default URIFetcher can be overwritten in order to customize its behaviour in regards of authorization or error handling. -Therefore the _URIFetcher_ interface must implemented and the method _fetch_ must be overwritten. - -``` -public class CustomUriFetcher implements URIFetcher { - - private static final Logger LOGGER = LoggerFactory.getLogger(CustomUriFetcher.class); - - private final String authorizationToken; - - private final HttpClient client; - - public CustomUriFetcher(String authorizationToken) { - this.authorizationToken = authorizationToken; - this.client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); - } - - @Override - public InputStream fetch(URI uri) throws IOException { - HttpRequest request = HttpRequest.newBuilder().uri(uri).header("Authorization", authorizationToken).build(); - try { - HttpResponse response = this.client.send(request, HttpResponse.BodyHandlers.ofString()); - if ((200 > response.statusCode()) || (response.statusCode() > 299)) { - String errorMessage = String.format("Could not get data from schema endpoint. The following status %d was returned.", response.statusCode()); - LOGGER.error(errorMessage); - } - - return new ByteArrayInputStream(response.body().getBytes(StandardCharsets.UTF_8)); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } -} -``` - -Within the _JsonSchemaFactory_ the custom URIFetcher can be referenced. -This also works for schema references ($ref) inside the schema. - -``` -... -CustomUriFetcher uriFetcher = new CustomUriFetcher(authorizationToken); - -JsonSchemaFactory schemaFactory = JsonSchemaFactory.builder() - .uriFetcher(uriFetcher, "http") - .addMetaSchema(JsonMetaSchema.getV7()) - .defaultMetaSchemaURI(JsonMetaSchema.getV7().getUri()) - .build(); -JsonSchema jsonSchema = schemaFactory.getSchema(schemaUri); -for (ValidationMessage validationMessage : jsonSchema.validate(jsonNodeRecord)) { - // handle the validation messages -} -``` - -**_NOTE:_** -Within `.uriFetcher(uriFetcher, "http")` your URI must be mapped to the related protocol like http, ftp, ... \ No newline at end of file diff --git a/doc/multiple-language.md b/doc/multiple-language.md index 73cb84079..92881501d 100644 --- a/doc/multiple-language.md +++ b/doc/multiple-language.md @@ -2,7 +2,7 @@ The error messages have been translated to several languages by contributors, de bundle under https://github.com/networknt/json-schema-validator/tree/master/src/main/resources. To use one of the available translations the simplest approach is to set your default locale before running the validation: -``` +```java // Set the default locale to German (needs only to be set once before using the validator) Locale.setDefault(Locale.GERMAN); JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); @@ -14,7 +14,7 @@ Note that the above approach changes the locale for the entire JVM which is prob using this in an application expected to support multiple languages (for example a localised web application). In this case you should use the `SchemaValidatorsConfig` class before loading your schema: -``` +```java // Set the configuration with a specific locale (you can create this before each validation) SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setLocale(myLocale); @@ -26,7 +26,7 @@ JsonSchema schema = factory.getSchema(source, config); Besides setting the locale and using the default resource bundle, you may also specify your own to cover any languages you choose without adapting the library's source, or to override default messages. In doing so you however you should ensure that your resource bundle covers all the keys defined by the default bundle. -``` +```java // Set the configuration with a custom message source MessageSource messageSource = new ResourceBundleMessageSource("my-messages"); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); @@ -38,7 +38,7 @@ JsonSchema schema = factory.getSchema(source, config); It is possible to override specific keys from the default resource bundle. Note however that you will need to supply all the languages for that specific key as it will not fallback on the default resource bundle. For instance the jsv-messages-override resource bundle will take precedence when resolving the message key. -``` +```java // Set the configuration with a custom message source MessageSource messageSource = new ResourceBundleMessageSource("jsv-messages-override", DefaultMessageSource.BUNDLE_BASE_NAME); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); @@ -50,7 +50,7 @@ JsonSchema schema = factory.getSchema(source, config); The following approach can be used to determine the locale to use on a per user basis using a language tag priority list. -``` +```java SchemaValidatorsConfig config = new SchemaValidatorsConfig(); JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); JsonSchema schema = factory.getSchema(source, config); diff --git a/doc/quickstart.md b/doc/quickstart.md index bb4ab07a9..65467b817 100644 --- a/doc/quickstart.md +++ b/doc/quickstart.md @@ -38,7 +38,7 @@ public class BaseJsonSchemaValidatorTest { protected JsonSchema getJsonSchemaFromUrl(String uri) throws URISyntaxException { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - return factory.getSchema(new URI(uri)); + return factory.getSchema(SchemaLocation.of(uri)); } protected JsonSchema getJsonSchemaFromJsonNode(JsonNode jsonNode) { diff --git a/doc/schema-map.md b/doc/schema-map.md deleted file mode 100644 index beb4559c8..000000000 --- a/doc/schema-map.md +++ /dev/null @@ -1,141 +0,0 @@ -While working with JSON schema validation, we have to use external references sometimes. However, there are two issues to have references to schemas on the Internet. - -* Some applications are running inside a corporate network without Internet access. -* Some of the Internet resources are not reliable. -* A test environment may serve unpublished schemas, which are not yet available at the location identified by the payload's `$schema` property. - -One solution is to change all the external reference to internal in JSON schemas, but this is error-prone and hard to maintain in a long run. - -A smart solution is to map the external references to internal ones in a configuration file. This allows us to use the resources as they are without any modification. In the JSON schema specification, it is not allowed to use local filesystem resource directly. With the mapping, we can use the local resources without worrying about breaking the specification as the references are still in URL format in schemas. In addition, the mapped URL can be a different external URL, or embbeded within a JAR file with a lot more flexibility. - -Note that when using a mapping, the local copy is always used, and the external reference is not queried. - -### URI Translation - -Both `SchemaValidatorsConfig` and `JsonSchemaFactory` accept one or more `URITranslator` instances. A `URITranslator` is responsible for providing a new URI when the given URI matches certain criteria. - -#### Examples - -Automatically map HTTP to HTTPS - -```java -SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -config.addUriTranlator(uri -> { - if ("http".equalsIgnoreCase(uri.getScheme()) { - try { - return new URI( - "https", - uri.getUserInfo(), - uri.getHost(), - uri.getPort(), - uri.getPath(), - uri.getQuery(), - uri.getFragment() - ); - } catch (URISyntaxException x) { - throw new IllegalArgumentException(x.getMessage(), x); - } - } - return uri; -}); -``` - -Map a public schema to a test environment - -```java -SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -config.addUriTranlator(uri -> { - if (true - && "https".equalsIgnoreCase(uri.getScheme() - && "schemas.acme.org".equalsIgnoreCase(uri.getHost()) - && (-1 == uri.getPort() || 443 == uri.getPort()) - ) { - try { - return new URI( - "http", - uri.getUserInfo(), - "test-schemas.acme.org", - 8080, - uri.getPath(), - uri.getQuery(), - uri.getFragment() - ); - } catch (URISyntaxException x) { - throw new IllegalArgumentException(x.getMessage(), x); - } - } - return uri; -}); -``` - -Replace a URI with another - -**Note:** -This also works for mapping URNs to resources. - -```java -SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -config.addUriTranlator(URITranslator.map("https://schemas.acme.org/Foo", "classpath://Foo"); -``` - -### Precedence - -Both `SchemaValidatorsConfig` and `JsonSchemaFactory` accept multiple `URITranslator`s and in general, they are evaluated in the order of addition. This means that each `URITranslator` receives the output of the previous translator. For example, assuming the following configuration: - -``` -SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -config.addUriTranlator(uri -> { - if ("http".equalsIgnoreCase(uri.getScheme()) { - try { - return new URI( - "https", - uri.getUserInfo(), - uri.getHost(), - uri.getPort(), - uri.getPath(), - uri.getQuery(), - uri.getFragment() - ); - } catch (URISyntaxException x) { - throw new IllegalArgumentException(x.getMessage(), x); - } - } - return uri; -}); -config.addUriTranlator(uri -> { - if (true - && "https".equalsIgnoreCase(uri.getScheme() - && "schemas.acme.org".equalsIgnoreCase(uri.getHost()) - && (-1 == uri.getPort() || 443 == uri.getPort()) - ) { - try { - return new URI( - "http", - uri.getUserInfo(), - "test-schemas.acme.org", - 8080, - uri.getPath(), - uri.getQuery(), - uri.getFragment() - ); - } catch (URISyntaxException x) { - throw new IllegalArgumentException(x.getMessage(), x); - } - } - return uri; -}); -config.addUriTranlator(URITranslator.map("http://test-schemas.acme.org:8080/Foo", "classpath://Foo"); -``` - -Given a starting URI of `https://schemas.acme.org/Foo`, the configuration above produces the following translations (in order): - -1. The translation from HTTP to HTTPS does not occur since the original URI already specifies HTTPS. -2. The second rule receives the original URI since nothing happened in the first rule. The second rule translates the URI from `https://schemas.acme.org/Foo` to `http://test-schemas.acme.org:8080/Foo` since the scheme, host and port match this rule. -3. The third rule receives the URI produced by the second rule and performs a simple mapping to a local resource. - -Since all `JsonSchemaFactory`s are created from an optional `SchemaValidatorsConfig`, any `URITranslator`s added to the factory are evaluated after those provided by `SchemaValidatorsConfig`. - -### Deprecated - -Previously, this library supported simple mappings from one URI to another through `SchemaValidatorsConfig.setUriMappings()` and `JsonSchemaFactory.addUriMappings()`. Usage of these methods are still supported but are now discouraged. `URITranslator` provides a more powerful mechanism of dealing with URI mapping than what was provided before. - diff --git a/doc/schema-retrieval.md b/doc/schema-retrieval.md new file mode 100644 index 000000000..08e37f480 --- /dev/null +++ b/doc/schema-retrieval.md @@ -0,0 +1,99 @@ +# Customizing Schema Retrieval + +A schema can be identified by its schema identifier which is indicated using the `$id` keyword or `id` keyword in earlier drafts. This is an absolute IRI that uniquely identifies the schema and is not necessarily a network locator. A schema need not be downloadable from it's absolute IRI. + +In the event a schema references a schema identifier that is not a subschema resource, for instance defined in the `$defs` keyword or `definitions` keyword. The library will need to be able to retrieve the schema given its schema identifier. + +In the event that the schema does not define a schema identifier using the `$id` keyword, the retrieval IRI will be used as it's schema identifier. + +## Mapping Schema Identifier to Retrieval IRI + +The schema identifier can be mapped to the retrieval IRI by implementing the `AbsoluteIriMapper` interface. + +### Configuring AbsoluteIriMapper + +```java +JsonSchemaFactory schemaFactory = JsonSchemaFactory.builder() + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder + .absoluteIriMapper(new CustomAbsoluteIriMapper()) + .addMetaSchema(JsonMetaSchema.getV7()) + .defaultMetaSchemaURI(JsonMetaSchema.getV7().getUri()) + .build(); +``` + +### Configuring Prefix Mappings + +```java +JsonSchemaFactory schemaFactory = JsonSchemaFactory.builder() + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder + .mapPrefix("https://", "http://") + .mapPrefix("http://json-schema.org", "classpath:")) + .addMetaSchema(JsonMetaSchema.getV7()) + .defaultMetaSchemaURI(JsonMetaSchema.getV7().getUri()) + .build(); +``` + +## Customizing Network Schema Retrieval + +The default `UriSchemaLoader` implementation uses JDK connection/socket without handling network exceptions. It works in most of the cases; however, if you want to have a customized implementation, you can do so. One user has his implementation with urirest to handle the timeout. A detailed discussion can be found in this [issue](https://github.com/networknt/json-schema-validator/issues/240) + +### Configuring Custom URI Schema Loader + +The default `UriSchemaLoader` can be overwritten in order to customize its behaviour in regards of authorization or error handling. + +The `SchemaLoader` interface must implemented and the implementation configured on the `JsonSchemaFactory`. + +```java +public class CustomUriSchemaLoader implements SchemaLoader { + + private static final Logger LOGGER = LoggerFactory.getLogger(CustomUriSchemaLoader.class); + + private final String authorizationToken; + + private final HttpClient client; + + public CustomUriSchemaLoader(String authorizationToken) { + this.authorizationToken = authorizationToken; + this.client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + } + + @Override + public InputStreamSource getSchema(SchemaLocation schemaLocation) { + URI uri = URI.create(schemaLocation.getAbsoluteIri().toString()); + return () -> { + HttpRequest request = HttpRequest.newBuilder().uri(uri).header("Authorization", authorizationToken).build(); + try { + HttpResponse response = this.client.send(request, HttpResponse.BodyHandlers.ofString()); + if ((200 > response.statusCode()) || (response.statusCode() > 299)) { + String errorMessage = String.format("Could not get data from schema endpoint. The following status %d was returned.", response.statusCode()); + LOGGER.error(errorMessage); + } + return new ByteArrayInputStream(response.body().getBytes(StandardCharsets.UTF_8)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } +} +``` + +Within the `JsonSchemaFactory` the custom `SchemaLoader` must be configured. + +```java +CustomUriSchemaLoader uriSchemaLoader = new CustomUriSchemaLoader(authorizationToken); + +JsonSchemaFactory schemaFactory = JsonSchemaFactory.builder() + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.schemaLoaders(schemaLoaders -> { + for (int x = 0; x < schemaLoaders.size(); x++) { + if (schemaLoaders.get(x) instanceof UriSchemaLoader) { + schemaLoaders.set(x, uriSchemaLoader); + } + } + .addMetaSchema(JsonMetaSchema.getV7()) + .defaultMetaSchemaURI(JsonMetaSchema.getV7().getUri()) + .build(); +JsonSchema jsonSchema = schemaFactory.getSchema(schemaUri); +for (ValidationMessage validationMessage : jsonSchema.validate(jsonNodeRecord)) { + // handle the validation messages +} +``` diff --git a/src/test/java/com/networknt/schema/CustomUriTest.java b/src/test/java/com/networknt/schema/CustomUriTest.java index b86a9dff4..9b35ef2fe 100644 --- a/src/test/java/com/networknt/schema/CustomUriTest.java +++ b/src/test/java/com/networknt/schema/CustomUriTest.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.uri.InputStreamSource; import com.networknt.schema.uri.SchemaLoader; +import com.networknt.schema.uri.UriSchemaLoader; + import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; @@ -31,7 +33,13 @@ public void customUri() throws Exception { private JsonSchemaFactory buildJsonSchemaFactory() { return JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.schemaLoader(new CustomUriFetcher())).build(); + .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.schemaLoaders(schemaLoaders -> { + for (int x = 0; x < schemaLoaders.size(); x++) { + if (schemaLoaders.get(x) instanceof UriSchemaLoader) { + schemaLoaders.set(x, new CustomUriFetcher()); + } + } + })).build(); } private static class CustomUriFetcher implements SchemaLoader { diff --git a/src/test/suite/tests/draft2019-09/refRemote.json b/src/test/suite/tests/draft2019-09/refRemote.json index b84dad69a..00bf60b5b 100644 --- a/src/test/suite/tests/draft2019-09/refRemote.json +++ b/src/test/suite/tests/draft2019-09/refRemote.json @@ -147,8 +147,6 @@ } } }, - "disabled": true, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", @@ -302,8 +300,6 @@ { "description": "remote HTTP ref with nested absolute ref", "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, - "disabled": true, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is invalid", diff --git a/src/test/suite/tests/draft2020-12/refRemote.json b/src/test/suite/tests/draft2020-12/refRemote.json index 5508e357f..17c36a29a 100644 --- a/src/test/suite/tests/draft2020-12/refRemote.json +++ b/src/test/suite/tests/draft2020-12/refRemote.json @@ -147,7 +147,6 @@ } } }, - "disabled": true, "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { @@ -302,8 +301,6 @@ { "description": "remote HTTP ref with nested absolute ref", "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, - "disabled": true, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is invalid", diff --git a/src/test/suite/tests/draft4/id.json b/src/test/suite/tests/draft4/id.json index d49133f74..1c91d33ee 100644 --- a/src/test/suite/tests/draft4/id.json +++ b/src/test/suite/tests/draft4/id.json @@ -40,16 +40,12 @@ { "description": "match $ref to id", "data": "a string to match #/definitions/id_in_enum", - "valid": true, - "disabled": true, - "reason": "TODO: Dereferencing an id is conditional on the contxt" + "valid": true }, { "description": "no match on enum or $ref to id", "data": 1, - "valid": false, - "disabled": true, - "reason": "TODO: Dereferencing an id is conditional on the contxt" + "valid": false } ] } From fee73b733dc506d47b746af715dba94c9c998e34 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 14:01:05 +0800 Subject: [PATCH 14/65] Refactor --- .../java/com/networknt/schema/SchemaValidatorsConfig.java | 5 ----- .../com/networknt/schema/AbstractJsonSchemaTestSuite.java | 1 - src/test/java/com/networknt/schema/CustomMetaSchemaTest.java | 1 - src/test/java/com/networknt/schema/Issue285Test.java | 2 -- src/test/java/com/networknt/schema/Issue451Test.java | 1 + src/test/java/com/networknt/schema/Issue619Test.java | 2 -- src/test/java/com/networknt/schema/Issue650Test.java | 1 - src/test/java/com/networknt/schema/Issue898Test.java | 4 ---- .../java/com/networknt/schema/JsonWalkApplyDefaultsTest.java | 1 - src/test/java/com/networknt/schema/OutputFormatTest.java | 1 - src/test/java/com/networknt/schema/SharedConfigTest.java | 1 - 11 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index 9c7655542..d2790f96b 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -19,11 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.i18n.DefaultMessageSource; import com.networknt.schema.i18n.MessageSource; -import com.networknt.schema.uri.AbsoluteIriMapper; -import com.networknt.schema.uri.ClasspathSchemaLoader; -import com.networknt.schema.uri.DefaultSchemaLoader; -import com.networknt.schema.uri.SchemaLoader; -import com.networknt.schema.uri.UriSchemaLoader; import com.networknt.schema.walk.JsonSchemaWalkListener; import java.util.ArrayList; diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java index 3a5325857..524c8b12f 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java @@ -21,7 +21,6 @@ import com.networknt.schema.suite.TestCase; import com.networknt.schema.suite.TestSource; import com.networknt.schema.suite.TestSpec; -import com.networknt.schema.uri.PrefixAbsoluteIriMapper; import org.junit.jupiter.api.AssertionFailureBuilder; import org.junit.jupiter.api.DynamicNode; diff --git a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java index 6ef1f0ab6..7e427c2ef 100644 --- a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java +++ b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java @@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test; import java.io.IOException; -import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashSet; import java.util.List; diff --git a/src/test/java/com/networknt/schema/Issue285Test.java b/src/test/java/com/networknt/schema/Issue285Test.java index 9c7b33e69..e64f8b673 100644 --- a/src/test/java/com/networknt/schema/Issue285Test.java +++ b/src/test/java/com/networknt/schema/Issue285Test.java @@ -1,9 +1,7 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.uri.PrefixAbsoluteIriMapper; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.IOException; diff --git a/src/test/java/com/networknt/schema/Issue451Test.java b/src/test/java/com/networknt/schema/Issue451Test.java index 8edc17842..e01e430d9 100644 --- a/src/test/java/com/networknt/schema/Issue451Test.java +++ b/src/test/java/com/networknt/schema/Issue451Test.java @@ -81,6 +81,7 @@ public void onWalkEnd(WalkEvent walkEvent, Set validationMess } private Map collector(ExecutionContext executionContext) { + @SuppressWarnings("unchecked") Map collector = (Map) executionContext.getCollectorContext().get(COLLECTOR_ID); if(collector == null) { collector = new HashMap<>(); diff --git a/src/test/java/com/networknt/schema/Issue619Test.java b/src/test/java/com/networknt/schema/Issue619Test.java index 6c075d943..0ea63a2c6 100644 --- a/src/test/java/com/networknt/schema/Issue619Test.java +++ b/src/test/java/com/networknt/schema/Issue619Test.java @@ -20,8 +20,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.net.URI; - import static com.networknt.schema.BaseJsonSchemaValidatorTest.getJsonNodeFromStringContent; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/src/test/java/com/networknt/schema/Issue650Test.java b/src/test/java/com/networknt/schema/Issue650Test.java index 37a29f73e..02bffbf25 100644 --- a/src/test/java/com/networknt/schema/Issue650Test.java +++ b/src/test/java/com/networknt/schema/Issue650Test.java @@ -1,6 +1,5 @@ package com.networknt.schema; -import static org.junit.jupiter.api.Assertions.*; import java.io.InputStream; import java.util.Set; import org.junit.jupiter.api.Assertions; diff --git a/src/test/java/com/networknt/schema/Issue898Test.java b/src/test/java/com/networknt/schema/Issue898Test.java index 6255fc24a..24b653c8e 100644 --- a/src/test/java/com/networknt/schema/Issue898Test.java +++ b/src/test/java/com/networknt/schema/Issue898Test.java @@ -2,14 +2,10 @@ import com.fasterxml.jackson.databind.JsonNode; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.io.InputStream; import java.util.List; import java.util.Locale; -import java.util.ResourceBundle; import static java.util.stream.Collectors.toList; diff --git a/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java b/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java index 0c95579f2..abb0b06a6 100644 --- a/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java +++ b/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java @@ -10,7 +10,6 @@ import java.util.Set; import java.util.stream.Collectors; import org.hamcrest.Matchers; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; diff --git a/src/test/java/com/networknt/schema/OutputFormatTest.java b/src/test/java/com/networknt/schema/OutputFormatTest.java index 48c46975a..dc6ef6c07 100644 --- a/src/test/java/com/networknt/schema/OutputFormatTest.java +++ b/src/test/java/com/networknt/schema/OutputFormatTest.java @@ -3,7 +3,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import java.io.InputStream; -import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; diff --git a/src/test/java/com/networknt/schema/SharedConfigTest.java b/src/test/java/com/networknt/schema/SharedConfigTest.java index 283d99985..6173872ef 100644 --- a/src/test/java/com/networknt/schema/SharedConfigTest.java +++ b/src/test/java/com/networknt/schema/SharedConfigTest.java @@ -1,6 +1,5 @@ package com.networknt.schema; -import java.net.URI; import java.util.Set; import org.junit.jupiter.api.Assertions; From 64d7a68fa7b75ea64093e5b54d12aac025d39778 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 14:31:18 +0800 Subject: [PATCH 15/65] Fix --- .../networknt/schema/DynamicRefValidator.java | 2 +- .../draft2020-12/detached-dynamicref.json | 13 ++ .../remotes/draft2020-12/detached-ref.json | 13 ++ .../suite/tests/draft2020-12/dynamicRef.json | 143 ++++++++++++++---- 4 files changed, 142 insertions(+), 29 deletions(-) create mode 100644 src/test/suite/remotes/draft2020-12/detached-dynamicref.json create mode 100644 src/test/suite/remotes/draft2020-12/detached-ref.json diff --git a/src/main/java/com/networknt/schema/DynamicRefValidator.java b/src/main/java/com/networknt/schema/DynamicRefValidator.java index 2efbbbf78..91eb1dd47 100644 --- a/src/main/java/com/networknt/schema/DynamicRefValidator.java +++ b/src/main/java/com/networknt/schema/DynamicRefValidator.java @@ -46,7 +46,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val // A $dynamicRef without a matching $dynamicAnchor in the same schema resource // behaves like a normal $ref to $anchor // A $dynamicRef without anchor in fragment behaves identical to $ref - JsonSchemaRef r = RefValidator.getRefSchema(parentSchema, validationContext, ref, evaluationPath); + JsonSchemaRef r = RefValidator.getRefSchema(parentSchema, validationContext, refValue, evaluationPath); if (r != null) { refSchema = r.getSchema(); } diff --git a/src/test/suite/remotes/draft2020-12/detached-dynamicref.json b/src/test/suite/remotes/draft2020-12/detached-dynamicref.json new file mode 100644 index 000000000..07cce1dac --- /dev/null +++ b/src/test/suite/remotes/draft2020-12/detached-dynamicref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft2020-12/detached-dynamicref.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "foo": { + "$dynamicRef": "#detached" + }, + "detached": { + "$dynamicAnchor": "detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/remotes/draft2020-12/detached-ref.json b/src/test/suite/remotes/draft2020-12/detached-ref.json new file mode 100644 index 000000000..9c2dca93c --- /dev/null +++ b/src/test/suite/remotes/draft2020-12/detached-ref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft2020-12/detached-ref.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "foo": { + "$ref": "#detached" + }, + "detached": { + "$anchor": "detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/tests/draft2020-12/dynamicRef.json b/src/test/suite/tests/draft2020-12/dynamicRef.json index 0f6ed4804..bff26ad61 100644 --- a/src/test/suite/tests/draft2020-12/dynamicRef.json +++ b/src/test/suite/tests/draft2020-12/dynamicRef.json @@ -392,45 +392,84 @@ "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://test.json-schema.org/dynamic-ref-with-multiple-paths/main", + "if": { + "properties": { + "kindOfList": { "const": "numbers" } + }, + "required": ["kindOfList"] + }, + "then": { "$ref": "numberList" }, + "else": { "$ref": "stringList" }, + "$defs": { - "inner": { - "$id": "inner", - "$dynamicAnchor": "foo", - "title": "inner", - "additionalProperties": { - "$dynamicRef": "#foo" + "genericList": { + "$id": "genericList", + "properties": { + "list": { + "items": { "$dynamicRef": "#itemType" } + } + }, + "$defs": { + "defaultItemType": { + "$comment": "Only needed to satisfy bookending requirement", + "$dynamicAnchor": "itemType" + } } + }, + "numberList": { + "$id": "numberList", + "$defs": { + "itemType": { + "$dynamicAnchor": "itemType", + "type": "number" + } + }, + "$ref": "genericList" + }, + "stringList": { + "$id": "stringList", + "$defs": { + "itemType": { + "$dynamicAnchor": "itemType", + "type": "string" + } + }, + "$ref": "genericList" } - }, - "if": { - "propertyNames": { - "pattern": "^[a-m]" - } - }, - "then": { - "title": "any type of node", - "$id": "anyLeafNode", - "$dynamicAnchor": "foo", - "$ref": "inner" - }, - "else": { - "title": "integer node", - "$id": "integerNode", - "$dynamicAnchor": "foo", - "type": [ "object", "integer" ], - "$ref": "inner" } }, "tests": [ { - "description": "recurse to anyLeafNode - floats are allowed", - "data": { "alpha": 1.1 }, + "description": "number list with number values", + "data": { + "kindOfList": "numbers", + "list": [1.1] + }, "valid": true }, { - "description": "recurse to integerNode - floats are not allowed", - "data": { "november": 1.1 }, + "description": "number list with string values", + "data": { + "kindOfList": "numbers", + "list": ["foo"] + }, + "valid": false + }, + { + "description": "string list with number values", + "data": { + "kindOfList": "strings", + "list": [1.1] + }, "valid": false + }, + { + "description": "string list with string values", + "data": { + "kindOfList": "strings", + "list": ["foo"] + }, + "valid": true } ] }, @@ -669,5 +708,53 @@ "valid": true } ] + }, + { + "description": "$ref to $dynamicRef finds detached $dynamicAnchor", + "schema": { + "$ref": "http://localhost:1234/draft2020-12/detached-dynamicref.json#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "$dynamicRef points to a boolean schema", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "true": true, + "false": false + }, + "properties": { + "true": { + "$dynamicRef": "#/$defs/true" + }, + "false": { + "$dynamicRef": "#/$defs/false" + } + } + }, + "tests": [ + { + "description": "follow $dynamicRef to a true schema", + "data": { "true": 1 }, + "valid": true + }, + { + "description": "follow $dynamicRef to a false schema", + "data": { "false": 1 }, + "valid": false + } + ] } ] From 364365df3e5914b6aa575bed56212f2430f713ec Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:37:47 +0800 Subject: [PATCH 16/65] Refactor --- .../com/networknt/schema/JsonMetaSchema.java | 21 ------------------- .../java/com/networknt/schema/JsonSchema.java | 6 ------ 2 files changed, 27 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index dfeed1fd7..037b7551c 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -242,27 +242,6 @@ public String readDynamicAnchor(JsonNode schemaNode) { return null; } - public JsonNode getNodeByFragmentRef(String ref, JsonNode node) { - boolean supportsAnchor = this.keywords.containsKey("$anchor"); - String refName = supportsAnchor ? ref.substring(1) : ref; - String fieldToRead = supportsAnchor ? "$anchor" : this.idKeyword; - - boolean nodeContainsRef = refName.equals(readText(node, fieldToRead)); - if (nodeContainsRef) { - return node; - } - - Iterator children = node.elements(); - while (children.hasNext()) { - JsonNode refNode = getNodeByFragmentRef(ref, children.next()); - if (refNode != null) { - return refNode; - } - } - - return null; - } - private static String readText(JsonNode node, String field) { JsonNode idNode = node.get(field); if (idNode == null || !idNode.isTextual()) { diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index e27441219..d19295479 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -186,13 +186,7 @@ public JsonNode getRefSchemaNode(String ref) { if (node.isMissingNode()) { node = handleNullNode(ref, schema); } - } else if ((ref.startsWith("#") && ref.length() > 1) || (ref.startsWith("urn:") && ref.length() > 4)) { - node = this.metaSchema.getNodeByFragmentRef(ref, node); - if (node == null) { - node = handleNullNode(ref, schema); - } } - return node; } From 35c43e37709d6c40ac5269eb417c93be269f1b9e Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:38:01 +0800 Subject: [PATCH 17/65] Update test suite --- src/test/suite/tests/draft2019-09/anchor.json | 90 ------------------- .../tests/draft2019-09/optional/anchor.json | 60 +++++++++++++ .../suite/tests/draft2019-09/optional/id.json | 53 +++++++++++ .../optional/refOfUnknownKeyword.json | 23 +++++ .../draft2019-09/optional/unknownKeyword.json | 57 ++++++++++++ src/test/suite/tests/draft2020-12/anchor.json | 90 ------------------- .../tests/draft2020-12/optional/anchor.json | 60 +++++++++++++ .../suite/tests/draft2020-12/optional/id.json | 53 +++++++++++ .../optional/refOfUnknownKeyword.json | 23 +++++ .../draft2020-12/optional/unknownKeyword.json | 57 ++++++++++++ 10 files changed, 386 insertions(+), 180 deletions(-) create mode 100644 src/test/suite/tests/draft2019-09/optional/anchor.json create mode 100644 src/test/suite/tests/draft2019-09/optional/id.json create mode 100644 src/test/suite/tests/draft2019-09/optional/unknownKeyword.json create mode 100644 src/test/suite/tests/draft2020-12/optional/anchor.json create mode 100644 src/test/suite/tests/draft2020-12/optional/id.json create mode 100644 src/test/suite/tests/draft2020-12/optional/unknownKeyword.json diff --git a/src/test/suite/tests/draft2019-09/anchor.json b/src/test/suite/tests/draft2019-09/anchor.json index 5d8c86f11..eb0a969a8 100644 --- a/src/test/suite/tests/draft2019-09/anchor.json +++ b/src/test/suite/tests/draft2019-09/anchor.json @@ -81,64 +81,6 @@ } ] }, - { - "description": "$anchor inside an enum is not a real identifier", - "comment": "the implementation must not be confused by an $anchor buried in the enum", - "schema": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$defs": { - "anchor_in_enum": { - "enum": [ - { - "$anchor": "my_anchor", - "type": "null" - } - ] - }, - "real_identifier_in_schema": { - "$anchor": "my_anchor", - "type": "string" - }, - "zzz_anchor_in_const": { - "const": { - "$anchor": "my_anchor", - "type": "null" - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/anchor_in_enum" }, - { "$ref": "#my_anchor" } - ] - }, - "tests": [ - { - "description": "exact match to enum, and type matches", - "data": { - "$anchor": "my_anchor", - "type": "null" - }, - "valid": true - }, - { - "description": "in implementations that strip $anchor, this may match either $def", - "data": { - "type": "null" - }, - "valid": false - }, - { - "description": "match $ref to $anchor", - "data": "a string to match #/$defs/anchor_in_enum", - "valid": true - }, - { - "description": "no match on enum or $ref to $anchor", - "data": 1, - "valid": false - } - ] - }, { "description": "same $anchor with different base uri", "schema": { @@ -175,38 +117,6 @@ } ] }, - { - "description": "non-schema object containing an $anchor property", - "schema": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$defs": { - "const_not_anchor": { - "const": { - "$anchor": "not_a_real_anchor" - } - } - }, - "if": { - "const": "skip not_a_real_anchor" - }, - "then": true, - "else" : { - "$ref": "#/$defs/const_not_anchor" - } - }, - "tests": [ - { - "description": "skip traversing definition for a valid result", - "data": "skip not_a_real_anchor", - "valid": true - }, - { - "description": "const at const_not_anchor does not match", - "data": 1, - "valid": false - } - ] - }, { "description": "invalid anchors", "comment": "Section 8.2.3", diff --git a/src/test/suite/tests/draft2019-09/optional/anchor.json b/src/test/suite/tests/draft2019-09/optional/anchor.json new file mode 100644 index 000000000..45951d0a3 --- /dev/null +++ b/src/test/suite/tests/draft2019-09/optional/anchor.json @@ -0,0 +1,60 @@ +[ + { + "description": "$anchor inside an enum is not a real identifier", + "comment": "the implementation must not be confused by an $anchor buried in the enum", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$defs": { + "anchor_in_enum": { + "enum": [ + { + "$anchor": "my_anchor", + "type": "null" + } + ] + }, + "real_identifier_in_schema": { + "$anchor": "my_anchor", + "type": "string" + }, + "zzz_anchor_in_const": { + "const": { + "$anchor": "my_anchor", + "type": "null" + } + } + }, + "anyOf": [ + { "$ref": "#/$defs/anchor_in_enum" }, + { "$ref": "#my_anchor" } + ] + }, + "tests": [ + { + "description": "exact match to enum, and type matches", + "data": { + "$anchor": "my_anchor", + "type": "null" + }, + "valid": true + }, + { + "description": "in implementations that strip $anchor, this may match either $def", + "data": { + "type": "null" + }, + "valid": false + }, + { + "description": "match $ref to $anchor", + "data": "a string to match #/$defs/anchor_in_enum", + "valid": true + }, + { + "description": "no match on enum or $ref to $anchor", + "data": 1, + "valid": false + } + ] + } +] diff --git a/src/test/suite/tests/draft2019-09/optional/id.json b/src/test/suite/tests/draft2019-09/optional/id.json new file mode 100644 index 000000000..4daa8f51f --- /dev/null +++ b/src/test/suite/tests/draft2019-09/optional/id.json @@ -0,0 +1,53 @@ +[ + { + "description": "$id inside an enum is not a real identifier", + "comment": "the implementation must not be confused by an $id buried in the enum", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$defs": { + "id_in_enum": { + "enum": [ + { + "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", + "type": "null" + } + ] + }, + "real_id_in_schema": { + "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", + "type": "string" + }, + "zzz_id_in_const": { + "const": { + "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", + "type": "null" + } + } + }, + "anyOf": [ + { "$ref": "#/$defs/id_in_enum" }, + { "$ref": "https://localhost:1234/draft2019-09/id/my_identifier.json" } + ] + }, + "tests": [ + { + "description": "exact match to enum, and type matches", + "data": { + "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", + "type": "null" + }, + "valid": true + }, + { + "description": "match $ref to $id", + "data": "a string to match #/$defs/id_in_enum", + "valid": true + }, + { + "description": "no match on enum or $ref to $id", + "data": 1, + "valid": false + } + ] + } +] diff --git a/src/test/suite/tests/draft2019-09/optional/refOfUnknownKeyword.json b/src/test/suite/tests/draft2019-09/optional/refOfUnknownKeyword.json index eee1c33ed..e9a75dd1e 100644 --- a/src/test/suite/tests/draft2019-09/optional/refOfUnknownKeyword.json +++ b/src/test/suite/tests/draft2019-09/optional/refOfUnknownKeyword.json @@ -42,5 +42,28 @@ "valid": false } ] + }, + { + "description": "reference internals of known non-applicator", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "/base", + "examples": [ + { "type": "string" } + ], + "$ref": "#/examples/0" + }, + "tests": [ + { + "description": "match", + "data": "a string", + "valid": true + }, + { + "description": "mismatch", + "data": 42, + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft2019-09/optional/unknownKeyword.json b/src/test/suite/tests/draft2019-09/optional/unknownKeyword.json new file mode 100644 index 000000000..f98e87c54 --- /dev/null +++ b/src/test/suite/tests/draft2019-09/optional/unknownKeyword.json @@ -0,0 +1,57 @@ +[ + { + "description": "$id inside an unknown keyword is not a real identifier", + "comment": "the implementation must not be confused by an $id in locations we do not know how to parse", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$defs": { + "id_in_unknown0": { + "not": { + "array_of_schemas": [ + { + "$id": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json", + "type": "null" + } + ] + } + }, + "real_id_in_schema": { + "$id": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json", + "type": "string" + }, + "id_in_unknown1": { + "not": { + "object_of_schemas": { + "foo": { + "$id": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json", + "type": "integer" + } + } + } + } + }, + "anyOf": [ + { "$ref": "#/$defs/id_in_unknown0" }, + { "$ref": "#/$defs/id_in_unknown1" }, + { "$ref": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json" } + ] + }, + "tests": [ + { + "description": "type matches second anyOf, which has a real schema in it", + "data": "a string", + "valid": true + }, + { + "description": "type matches non-schema in first anyOf", + "data": null, + "valid": false + }, + { + "description": "type matches non-schema in third anyOf", + "data": 1, + "valid": false + } + ] + } +] diff --git a/src/test/suite/tests/draft2020-12/anchor.json b/src/test/suite/tests/draft2020-12/anchor.json index 423835dac..83a7166d7 100644 --- a/src/test/suite/tests/draft2020-12/anchor.json +++ b/src/test/suite/tests/draft2020-12/anchor.json @@ -81,64 +81,6 @@ } ] }, - { - "description": "$anchor inside an enum is not a real identifier", - "comment": "the implementation must not be confused by an $anchor buried in the enum", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "anchor_in_enum": { - "enum": [ - { - "$anchor": "my_anchor", - "type": "null" - } - ] - }, - "real_identifier_in_schema": { - "$anchor": "my_anchor", - "type": "string" - }, - "zzz_anchor_in_const": { - "const": { - "$anchor": "my_anchor", - "type": "null" - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/anchor_in_enum" }, - { "$ref": "#my_anchor" } - ] - }, - "tests": [ - { - "description": "exact match to enum, and type matches", - "data": { - "$anchor": "my_anchor", - "type": "null" - }, - "valid": true - }, - { - "description": "in implementations that strip $anchor, this may match either $def", - "data": { - "type": "null" - }, - "valid": false - }, - { - "description": "match $ref to $anchor", - "data": "a string to match #/$defs/anchor_in_enum", - "valid": true - }, - { - "description": "no match on enum or $ref to $anchor", - "data": 1, - "valid": false - } - ] - }, { "description": "same $anchor with different base uri", "schema": { @@ -175,38 +117,6 @@ } ] }, - { - "description": "non-schema object containing an $anchor property", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "const_not_anchor": { - "const": { - "$anchor": "not_a_real_anchor" - } - } - }, - "if": { - "const": "skip not_a_real_anchor" - }, - "then": true, - "else" : { - "$ref": "#/$defs/const_not_anchor" - } - }, - "tests": [ - { - "description": "skip traversing definition for a valid result", - "data": "skip not_a_real_anchor", - "valid": true - }, - { - "description": "const at const_not_anchor does not match", - "data": 1, - "valid": false - } - ] - }, { "description": "invalid anchors", "comment": "Section 8.2.2", diff --git a/src/test/suite/tests/draft2020-12/optional/anchor.json b/src/test/suite/tests/draft2020-12/optional/anchor.json new file mode 100644 index 000000000..6d6713be5 --- /dev/null +++ b/src/test/suite/tests/draft2020-12/optional/anchor.json @@ -0,0 +1,60 @@ +[ + { + "description": "$anchor inside an enum is not a real identifier", + "comment": "the implementation must not be confused by an $anchor buried in the enum", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "anchor_in_enum": { + "enum": [ + { + "$anchor": "my_anchor", + "type": "null" + } + ] + }, + "real_identifier_in_schema": { + "$anchor": "my_anchor", + "type": "string" + }, + "zzz_anchor_in_const": { + "const": { + "$anchor": "my_anchor", + "type": "null" + } + } + }, + "anyOf": [ + { "$ref": "#/$defs/anchor_in_enum" }, + { "$ref": "#my_anchor" } + ] + }, + "tests": [ + { + "description": "exact match to enum, and type matches", + "data": { + "$anchor": "my_anchor", + "type": "null" + }, + "valid": true + }, + { + "description": "in implementations that strip $anchor, this may match either $def", + "data": { + "type": "null" + }, + "valid": false + }, + { + "description": "match $ref to $anchor", + "data": "a string to match #/$defs/anchor_in_enum", + "valid": true + }, + { + "description": "no match on enum or $ref to $anchor", + "data": 1, + "valid": false + } + ] + } +] diff --git a/src/test/suite/tests/draft2020-12/optional/id.json b/src/test/suite/tests/draft2020-12/optional/id.json new file mode 100644 index 000000000..0b7df4e80 --- /dev/null +++ b/src/test/suite/tests/draft2020-12/optional/id.json @@ -0,0 +1,53 @@ +[ + { + "description": "$id inside an enum is not a real identifier", + "comment": "the implementation must not be confused by an $id buried in the enum", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "id_in_enum": { + "enum": [ + { + "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", + "type": "null" + } + ] + }, + "real_id_in_schema": { + "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", + "type": "string" + }, + "zzz_id_in_const": { + "const": { + "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", + "type": "null" + } + } + }, + "anyOf": [ + { "$ref": "#/$defs/id_in_enum" }, + { "$ref": "https://localhost:1234/draft2020-12/id/my_identifier.json" } + ] + }, + "tests": [ + { + "description": "exact match to enum, and type matches", + "data": { + "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", + "type": "null" + }, + "valid": true + }, + { + "description": "match $ref to $id", + "data": "a string to match #/$defs/id_in_enum", + "valid": true + }, + { + "description": "no match on enum or $ref to $id", + "data": 1, + "valid": false + } + ] + } +] diff --git a/src/test/suite/tests/draft2020-12/optional/refOfUnknownKeyword.json b/src/test/suite/tests/draft2020-12/optional/refOfUnknownKeyword.json index f91c18884..c2b080a1e 100644 --- a/src/test/suite/tests/draft2020-12/optional/refOfUnknownKeyword.json +++ b/src/test/suite/tests/draft2020-12/optional/refOfUnknownKeyword.json @@ -42,5 +42,28 @@ "valid": false } ] + }, + { + "description": "reference internals of known non-applicator", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "/base", + "examples": [ + { "type": "string" } + ], + "$ref": "#/examples/0" + }, + "tests": [ + { + "description": "match", + "data": "a string", + "valid": true + }, + { + "description": "mismatch", + "data": 42, + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft2020-12/optional/unknownKeyword.json b/src/test/suite/tests/draft2020-12/optional/unknownKeyword.json new file mode 100644 index 000000000..28b0c4ce9 --- /dev/null +++ b/src/test/suite/tests/draft2020-12/optional/unknownKeyword.json @@ -0,0 +1,57 @@ +[ + { + "description": "$id inside an unknown keyword is not a real identifier", + "comment": "the implementation must not be confused by an $id in locations we do not know how to parse", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "id_in_unknown0": { + "not": { + "array_of_schemas": [ + { + "$id": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json", + "type": "null" + } + ] + } + }, + "real_id_in_schema": { + "$id": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json", + "type": "string" + }, + "id_in_unknown1": { + "not": { + "object_of_schemas": { + "foo": { + "$id": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json", + "type": "integer" + } + } + } + } + }, + "anyOf": [ + { "$ref": "#/$defs/id_in_unknown0" }, + { "$ref": "#/$defs/id_in_unknown1" }, + { "$ref": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json" } + ] + }, + "tests": [ + { + "description": "type matches second anyOf, which has a real schema in it", + "data": "a string", + "valid": true + }, + { + "description": "type matches non-schema in first anyOf", + "data": null, + "valid": false + }, + { + "description": "type matches non-schema in third anyOf", + "data": 1, + "valid": false + } + ] + } +] From 113c1047a29d30e00a59aa074b01bff3be415b9c Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:38:14 +0800 Subject: [PATCH 18/65] Refactor --- .../networknt/schema/CollectorContext.java | 4 +-- .../networknt/schema/DynamicRefValidator.java | 5 ++-- .../java/com/networknt/schema/JsonSchema.java | 26 +++++++++++-------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/networknt/schema/CollectorContext.java b/src/main/java/com/networknt/schema/CollectorContext.java index 36647ef41..07b0c6800 100644 --- a/src/main/java/com/networknt/schema/CollectorContext.java +++ b/src/main/java/com/networknt/schema/CollectorContext.java @@ -100,12 +100,12 @@ public JsonSchema getOutermostSchema() { } JsonSchema lexicalRoot = context.findLexicalRoot(); - if (lexicalRoot.isDynamicAnchor()) { + if (lexicalRoot.isRecursiveAnchor()) { Iterator it = this.dynamicScopes.descendingIterator(); while (it.hasNext()) { Scope scope = it.next(); JsonSchema containingSchema = scope.getContainingSchema(); - if (null != containingSchema && containingSchema.isDynamicAnchor()) { + if (null != containingSchema && containingSchema.isRecursiveAnchor()) { return containingSchema; } } diff --git a/src/main/java/com/networknt/schema/DynamicRefValidator.java b/src/main/java/com/networknt/schema/DynamicRefValidator.java index 91eb1dd47..f1f0fd742 100644 --- a/src/main/java/com/networknt/schema/DynamicRefValidator.java +++ b/src/main/java/com/networknt/schema/DynamicRefValidator.java @@ -58,8 +58,9 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val String absoluteIri = ref.substring(0, index); while (base.getEvaluationParentSchema() != null) { base = base.getEvaluationParentSchema(); - if (!base.getSchemaLocation().getAbsoluteIri().toString().equals(absoluteIri)) { - absoluteIri = base.getSchemaLocation().getAbsoluteIri().toString(); + String baseAbsoluteIri = base.getSchemaLocation().getAbsoluteIri() != null ? base.getSchemaLocation().getAbsoluteIri().toString() : ""; + if (!baseAbsoluteIri.equals(absoluteIri)) { + absoluteIri = baseAbsoluteIri; String parentRef = SchemaLocation.resolve(base.getSchemaLocation(), anchor); JsonSchema parentRefSchema = validationContext.getDynamicAnchors().get(parentRef); if (parentRefSchema != null) { diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index d19295479..94a92848f 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -41,7 +41,7 @@ public class JsonSchema extends BaseJsonValidator { private List validators; private final JsonMetaSchema metaSchema; private boolean validatorsLoaded = false; - private boolean dynamicAnchor = false; + private boolean recursiveAnchor = false; private JsonValidator requiredValidator = null; private TypeValidator typeValidator; @@ -49,7 +49,6 @@ public class JsonSchema extends BaseJsonValidator { WalkListenerRunner keywordWalkListenerRunner = null; private final String id; - private final String anchor; static JsonSchema from(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { return new JsonSchema(validationContext, schemaLocation, evaluationPath, schemaNode, parent, suppressSubSchemaRetrieval); @@ -62,19 +61,25 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc this.metaSchema = this.validationContext.getMetaSchema(); initializeConfig(); this.id = this.validationContext.resolveSchemaId(this.schemaNode); - this.anchor = this.validationContext.getMetaSchema().readAnchor(this.schemaNode); if (this.id != null) { this.validationContext.getSchemaResources() .putIfAbsent(this.schemaLocation != null ? this.schemaLocation.toString() : this.id, this); } - if (this.anchor != null) { + String anchor = this.validationContext.getMetaSchema().readAnchor(this.schemaNode); + if (anchor != null) { + String absoluteIri = this.schemaLocation.getAbsoluteIri() != null + ? this.schemaLocation.getAbsoluteIri().toString() + : ""; this.validationContext.getSchemaResources() - .putIfAbsent(this.schemaLocation.getAbsoluteIri().toString() + "#" + anchor, this); + .putIfAbsent(absoluteIri + "#" + anchor, this); } String dynamicAnchor = this.validationContext.getMetaSchema().readDynamicAnchor(schemaNode); if (dynamicAnchor != null) { + String absoluteIri = this.schemaLocation.getAbsoluteIri() != null + ? this.schemaLocation.getAbsoluteIri().toString() + : ""; this.validationContext.getDynamicAnchors() - .putIfAbsent(this.schemaLocation.getAbsoluteIri().toString() + "#" + dynamicAnchor, this); + .putIfAbsent(absoluteIri + "#" + dynamicAnchor, this); } getValidators(); } @@ -96,12 +101,11 @@ protected JsonSchema(JsonSchema copy) { this.validators = copy.validators; this.metaSchema = copy.metaSchema; this.validatorsLoaded = copy.validatorsLoaded; - this.dynamicAnchor = copy.dynamicAnchor; + this.recursiveAnchor = copy.recursiveAnchor; this.requiredValidator = copy.requiredValidator; this.typeValidator = copy.typeValidator; this.keywordWalkListenerRunner = copy.keywordWalkListenerRunner; this.id = copy.id; - this.anchor = copy.anchor; } /** @@ -356,7 +360,7 @@ private List read(JsonNode schemaNode) { .build(); throw new JsonSchemaException(validationMessage); } - this.dynamicAnchor = nodeToUse.booleanValue(); + this.recursiveAnchor = nodeToUse.booleanValue(); } JsonValidator validator = this.validationContext.newValidator(schemaPath, path, @@ -717,8 +721,8 @@ public void initializeValidators() { } } - public boolean isDynamicAnchor() { - return this.dynamicAnchor; + public boolean isRecursiveAnchor() { + return this.recursiveAnchor; } /** From d5a48983b6fbaa13c9f16f919cc4f5b492374854 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:29:36 +0800 Subject: [PATCH 19/65] Fix --- .../java/com/networknt/schema/uri/DefaultSchemaLoader.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java b/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java index 4707a9bab..5af7961ce 100644 --- a/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java +++ b/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java @@ -35,15 +35,16 @@ public DefaultSchemaLoader(List schemaLoaders, List Date: Mon, 22 Jan 2024 17:30:02 +0800 Subject: [PATCH 20/65] Support custom meta schema --- .../networknt/schema/JsonSchemaFactory.java | 22 +++++++++++-------- .../networknt/schema/SpecVersionDetector.java | 16 +++++++++----- .../schema/AbstractJsonSchemaTest.java | 2 +- .../schema/AbstractJsonSchemaTestSuite.java | 4 ++-- .../schema/JsonSchemaTestSuiteTest.java | 2 +- .../schema/SpecVersionDetectorTest.java | 2 +- 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index d7a3f6c4b..79cb192d5 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -211,7 +211,7 @@ private JsonSchema doCreate(ValidationContext validationContext, SchemaLocation } private ValidationContext withMetaSchema(ValidationContext validationContext, JsonNode schemaNode) { - JsonMetaSchema metaSchema = getMetaSchema(schemaNode); + JsonMetaSchema metaSchema = getMetaSchema(schemaNode, validationContext.getConfig()); if (metaSchema != null && !metaSchema.getUri().equals(validationContext.getMetaSchema().getUri())) { return new ValidationContext(metaSchema, validationContext.getJsonSchemaFactory(), validationContext.getConfig(), validationContext.getSchemaReferences(), @@ -238,34 +238,38 @@ protected SchemaLocation getSchemaLocation(SchemaLocation schemaRetrievalUri, Js } protected ValidationContext createValidationContext(final JsonNode schemaNode, SchemaValidatorsConfig config) { - final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); + final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode, config); return new ValidationContext(jsonMetaSchema, this, config); } - private JsonMetaSchema getMetaSchema(final JsonNode schemaNode) { + private JsonMetaSchema getMetaSchema(final JsonNode schemaNode, SchemaValidatorsConfig config) { final JsonNode uriNode = schemaNode.get("$schema"); if (uriNode != null && uriNode.isTextual()) { - return jsonMetaSchemas.computeIfAbsent(normalizeMetaSchemaUri(uriNode.textValue()), this::fromId); + return jsonMetaSchemas.computeIfAbsent(normalizeMetaSchemaUri(uriNode.textValue()), id -> fromId(id, config)); } return null; } - private JsonMetaSchema findMetaSchemaForSchema(final JsonNode schemaNode) { + private JsonMetaSchema findMetaSchemaForSchema(final JsonNode schemaNode, SchemaValidatorsConfig config) { final JsonNode uriNode = schemaNode.get("$schema"); if (uriNode != null && !uriNode.isNull() && !uriNode.isTextual()) { throw new JsonSchemaException("Unknown MetaSchema: " + uriNode.toString()); } final String uri = uriNode == null || uriNode.isNull() ? defaultMetaSchemaURI : normalizeMetaSchemaUri(uriNode.textValue()); - final JsonMetaSchema jsonMetaSchema = jsonMetaSchemas.computeIfAbsent(uri, this::fromId); + final JsonMetaSchema jsonMetaSchema = jsonMetaSchemas.computeIfAbsent(uri, id -> fromId(id, config)); return jsonMetaSchema; } - private JsonMetaSchema fromId(String id) { + private JsonMetaSchema fromId(String id, SchemaValidatorsConfig config) { // Is it a well-known dialect? return SpecVersionDetector.detectOptionalVersion(id) .map(JsonSchemaFactory::checkVersion) .map(JsonSchemaVersion::getInstance) - .orElseThrow(() -> new JsonSchemaException("Unknown MetaSchema: " + id)); + .orElseGet(() -> { + // Custom meta schema + JsonSchema schema = getSchema(SchemaLocation.of(id), config); + return schema.getValidationContext().getMetaSchema(); + }); } public JsonSchema getSchema(final String schema, final SchemaValidatorsConfig config) { @@ -318,7 +322,7 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid schemaNode = jsonMapper.readTree(inputStream); } - final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); + final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode, config); JsonNodePath evaluationPath = new JsonNodePath(config.getPathType()); JsonSchema jsonSchema; SchemaLocation schemaLocation = SchemaLocation.of(schemaUri.toString()); diff --git a/src/main/java/com/networknt/schema/SpecVersionDetector.java b/src/main/java/com/networknt/schema/SpecVersionDetector.java index 43e80897a..118fe0669 100644 --- a/src/main/java/com/networknt/schema/SpecVersionDetector.java +++ b/src/main/java/com/networknt/schema/SpecVersionDetector.java @@ -58,7 +58,7 @@ private SpecVersionDetector() { * @return Spec version if present, otherwise throws an exception */ public static VersionFlag detect(JsonNode jsonNode) { - return detectOptionalVersion(jsonNode).orElseThrow( + return detectOptionalVersion(jsonNode, true).orElseThrow( () -> new JsonSchemaException("'" + SCHEMA_TAG + "' tag is not present") ); } @@ -70,23 +70,27 @@ public static VersionFlag detect(JsonNode jsonNode) { * @param jsonNode JSON Node to read from * @return Spec version if present, otherwise empty */ - public static Optional detectOptionalVersion(JsonNode jsonNode) { + public static Optional detectOptionalVersion(JsonNode jsonNode, boolean throwIfUnsupported) { return Optional.ofNullable(jsonNode.get(SCHEMA_TAG)).map(schemaTag -> { String schemaTagValue = schemaTag.asText(); String schemaUri = JsonSchemaFactory.normalizeMetaSchemaUri(schemaTagValue); - return VersionFlag.fromId(schemaUri) - .orElseThrow(() -> new JsonSchemaException("'" + schemaTagValue + "' is unrecognizable schema")); + if (throwIfUnsupported) { + return VersionFlag.fromId(schemaUri) + .orElseThrow(() -> new JsonSchemaException("'" + schemaTagValue + "' is unrecognizable schema")); + } else { + return VersionFlag.fromId(schemaUri).orElse(null); + } }); } // For 2019-09 and later published drafts, implementations that are able to // detect the draft of each schema via $schema SHOULD be configured to do so - public static VersionFlag detectVersion(JsonNode jsonNode, Path specification, VersionFlag defaultVersion) { + public static VersionFlag detectVersion(JsonNode jsonNode, Path specification, VersionFlag defaultVersion, boolean throwIfUnsupported) { return Stream.of( - detectOptionalVersion(jsonNode), + detectOptionalVersion(jsonNode, throwIfUnsupported), detectVersionFromPath(specification) ) .filter(Optional::isPresent) diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java index 44d5bd1ee..fc718acf9 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java @@ -62,7 +62,7 @@ private JsonNode getJsonNodeFromPath(String dataPath) { private JsonSchema getJsonSchema(JsonNode schemaNode) { return JsonSchemaFactory - .getInstance(SpecVersionDetector.detectOptionalVersion(schemaNode).orElse(DEFAULT_VERSION_FLAG)) + .getInstance(SpecVersionDetector.detectOptionalVersion(schemaNode, false).orElse(DEFAULT_VERSION_FLAG)) .getSchema(schemaNode); } diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java index 524c8b12f..5a33479fa 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java @@ -178,7 +178,7 @@ private DynamicNode buildContainer(VersionFlag defaultVersion, TestCase testCase private JsonSchemaFactory buildValidatorFactory(VersionFlag defaultVersion, TestCase testCase) { if (testCase.isDisabled()) return null; - VersionFlag specVersion = detectVersion(testCase.getSchema(), testCase.getSpecification(), defaultVersion); + VersionFlag specVersion = detectVersion(testCase.getSchema(), testCase.getSpecification(), defaultVersion, false); JsonSchemaFactory base = JsonSchemaFactory.getInstance(specVersion); return JsonSchemaFactory .builder(base) @@ -216,7 +216,7 @@ private DynamicNode buildTest(JsonSchemaFactory validatorFactory, TestSpec testS } SchemaLocation testCaseFileUri = SchemaLocation.of("classpath:" + toForwardSlashPath(testSpec.getTestCase().getSpecification())); - JsonSchema schema = validatorFactory.getSchema(testCaseFileUri, testSpec.getTestCase().getSchema(), config); + JsonSchema schema = validatorFactory.getSchema(/*testCaseFileUri, */testSpec.getTestCase().getSchema(), config); return dynamicTest(testSpec.getDescription(), () -> executeAndReset(schema, testSpec)); } diff --git a/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java b/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java index 0c09ff168..a1a398711 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java @@ -69,7 +69,7 @@ protected Optional reason(Path path) { } private void disableV202012Tests() { - this.disabled.put(Paths.get("src/test/suite/tests/draft2020-12/optional/format-assertion.json"), "Unsupported behavior"); + //this.disabled.put(Paths.get("src/test/suite/tests/draft2020-12/optional/format-assertion.json"), "Unsupported behavior"); this.disabled.put(Paths.get("src/test/suite/tests/draft2020-12/vocabulary.json"), "Unsupported behavior"); } diff --git a/src/test/java/com/networknt/schema/SpecVersionDetectorTest.java b/src/test/java/com/networknt/schema/SpecVersionDetectorTest.java index 13a689f1e..2c7804242 100644 --- a/src/test/java/com/networknt/schema/SpecVersionDetectorTest.java +++ b/src/test/java/com/networknt/schema/SpecVersionDetectorTest.java @@ -50,7 +50,7 @@ void detectOptionalSpecVersion() throws IOException { InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream( "data/schemaTagMissing.json"); JsonNode node = mapper.readTree(in); - Optional flag = SpecVersionDetector.detectOptionalVersion(node); + Optional flag = SpecVersionDetector.detectOptionalVersion(node, true); assertEquals(Optional.empty(), flag); } } From 339e096a2d703a503b819564efcc544b759ee612 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 19:05:00 +0800 Subject: [PATCH 21/65] Refactor --- .../com/networknt/schema/JsonMetaSchema.java | 41 +++++++++++++++++-- .../networknt/schema/JsonSchemaFactory.java | 18 +++++++- .../com/networknt/schema/Version201909.java | 7 ++++ .../com/networknt/schema/Version202012.java | 8 ++++ .../java/com/networknt/schema/Version4.java | 1 + .../java/com/networknt/schema/Version6.java | 1 + .../java/com/networknt/schema/Version7.java | 1 + .../schema/AbstractJsonSchemaTestSuite.java | 2 +- 8 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index 037b7551c..0f2d8905a 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -17,6 +17,7 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.format.DateFormat; import com.networknt.schema.format.EmailFormat; import com.networknt.schema.format.IdnEmailFormat; @@ -81,8 +82,10 @@ static PatternFormat pattern(String name, String regex) { } public static class Builder { + private VersionFlag specification = null; private Map keywords = new HashMap<>(); private Map formats = new HashMap<>(); + private Map vocabularies = new HashMap<>(); private String uri; private String idKeyword = "id"; @@ -133,6 +136,24 @@ public Builder addFormats(Collection formats) { return this; } + public Builder vocabulary(String vocabulary) { + return vocabulary(vocabulary, true); + } + + public Builder vocabulary(String vocabulary, boolean enabled) { + this.vocabularies.put(vocabulary, enabled); + return this; + } + + public Builder vocabularies(Map vocabularies) { + this.vocabularies = vocabularies; + return this; + } + + public Builder specification(VersionFlag specification) { + this.specification = specification; + return this; + } public Builder idKeyword(String idKeyword) { this.idKeyword = idKeyword; @@ -142,15 +163,17 @@ public Builder idKeyword(String idKeyword) { public JsonMetaSchema build() { // create builtin keywords with (custom) formats. Map kwords = createKeywordsMap(this.keywords, this.formats); - return new JsonMetaSchema(this.uri, this.idKeyword, kwords); + return new JsonMetaSchema(this.uri, this.idKeyword, kwords, this.vocabularies, this.specification); } } private final String uri; private final String idKeyword; private Map keywords; + private Map vocabularies; + private final VersionFlag specification; - JsonMetaSchema(String uri, String idKeyword, Map keywords) { + JsonMetaSchema(String uri, String idKeyword, Map keywords, Map vocabularies, VersionFlag specification) { if (StringUtils.isBlank(uri)) { throw new IllegalArgumentException("uri must not be null or blank"); } @@ -164,6 +187,7 @@ public JsonMetaSchema build() { this.uri = uri; this.idKeyword = idKeyword; this.keywords = keywords; + this.specification = specification; } public static JsonMetaSchema getV4() { @@ -215,7 +239,10 @@ public static Builder builder(String uri, JsonMetaSchema blueprint) { return builder(uri) .idKeyword(blueprint.idKeyword) .addKeywords(blueprint.keywords.values()) - .addFormats(formatKeyword.getFormats()); + .addFormats(formatKeyword.getFormats()) + .specification(blueprint.getSpecification()) + .vocabularies(blueprint.getVocabularies()) + ; } public String getIdKeyword() { @@ -258,6 +285,14 @@ public Map getKeywords() { return this.keywords; } + public Map getVocabularies() { + return this.vocabularies; + } + + public VersionFlag getSpecification() { + return this.specification; + } + public JsonValidator newValidator(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, String keyword /* keyword */, JsonNode schemaNode, JsonSchema parentSchema) { diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 79cb192d5..b443a5413 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.uri.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +30,7 @@ import java.net.URISyntaxException; import java.util.Collection; import java.util.Map; +import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Consumer; @@ -268,7 +270,21 @@ private JsonMetaSchema fromId(String id, SchemaValidatorsConfig config) { .orElseGet(() -> { // Custom meta schema JsonSchema schema = getSchema(SchemaLocation.of(id), config); - return schema.getValidationContext().getMetaSchema(); + JsonMetaSchema.Builder builder = JsonMetaSchema.builder(id, schema.getValidationContext().getMetaSchema()); + VersionFlag specification = schema.getValidationContext().getMetaSchema().getSpecification(); + if (specification != null) { + if (specification.getVersionFlagValue() >= VersionFlag.V201909.getVersionFlagValue()) { + // Process vocabularies + JsonNode vocabulary = schema.getSchemaNode().get("$vocabulary"); + if (vocabulary != null) { + for(Entry vocabs : vocabulary.properties()) { + builder.vocabulary(vocabs.getKey(), vocabs.getValue().booleanValue()); + } + } + + } + } + return builder.build(); }); } diff --git a/src/main/java/com/networknt/schema/Version201909.java b/src/main/java/com/networknt/schema/Version201909.java index 6410bd994..75b22ef06 100644 --- a/src/main/java/com/networknt/schema/Version201909.java +++ b/src/main/java/com/networknt/schema/Version201909.java @@ -13,6 +13,7 @@ public class Version201909 extends JsonSchemaVersion{ @Override public JsonMetaSchema getInstance() { return new JsonMetaSchema.Builder(URI) + .specification(SpecVersion.VersionFlag.V201909) .idKeyword(ID) .addFormats(BUILTIN_FORMATS) .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V201909)) @@ -37,6 +38,12 @@ public JsonMetaSchema getInstance() { new NonValidationKeyword("then"), new NonValidationKeyword("else") )) + .vocabulary("https://json-schema.org/draft/2019-09/vocab/core") + .vocabulary("https://json-schema.org/draft/2019-09/vocab/applicator") + .vocabulary("https://json-schema.org/draft/2019-09/vocab/validation") + .vocabulary("https://json-schema.org/draft/2019-09/vocab/meta-data") + .vocabulary("https://json-schema.org/draft/2019-09/vocab/format", false) + .vocabulary("https://json-schema.org/draft/2019-09/vocab/content") .build(); } } diff --git a/src/main/java/com/networknt/schema/Version202012.java b/src/main/java/com/networknt/schema/Version202012.java index 3d07de784..8cba2a48f 100644 --- a/src/main/java/com/networknt/schema/Version202012.java +++ b/src/main/java/com/networknt/schema/Version202012.java @@ -14,6 +14,7 @@ public class Version202012 extends JsonSchemaVersion { @Override public JsonMetaSchema getInstance() { return new JsonMetaSchema.Builder(URI) + .specification(SpecVersion.VersionFlag.V202012) .idKeyword(ID) .addFormats(BUILTIN_FORMATS) .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V202012)) @@ -37,6 +38,13 @@ public JsonMetaSchema getInstance() { new NonValidationKeyword("else"), new NonValidationKeyword("additionalItems") )) + .vocabulary("https://json-schema.org/draft/2020-12/vocab/core") + .vocabulary("https://json-schema.org/draft/2020-12/vocab/applicator") + .vocabulary("https://json-schema.org/draft/2020-12/vocab/unevaluated") + .vocabulary("https://json-schema.org/draft/2020-12/vocab/validation") + .vocabulary("https://json-schema.org/draft/2020-12/vocab/meta-data") + .vocabulary("https://json-schema.org/draft/2020-12/vocab/format-annotation") + .vocabulary("https://json-schema.org/draft/2020-12/vocab/content") .build(); } } diff --git a/src/main/java/com/networknt/schema/Version4.java b/src/main/java/com/networknt/schema/Version4.java index 5763118d9..248dad904 100644 --- a/src/main/java/com/networknt/schema/Version4.java +++ b/src/main/java/com/networknt/schema/Version4.java @@ -13,6 +13,7 @@ public class Version4 extends JsonSchemaVersion{ public JsonMetaSchema getInstance() { return new JsonMetaSchema.Builder(URI) + .specification(SpecVersion.VersionFlag.V4) .idKeyword(ID) .addFormats(BUILTIN_FORMATS) .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V4)) diff --git a/src/main/java/com/networknt/schema/Version6.java b/src/main/java/com/networknt/schema/Version6.java index c9243bac1..4459bb889 100644 --- a/src/main/java/com/networknt/schema/Version6.java +++ b/src/main/java/com/networknt/schema/Version6.java @@ -14,6 +14,7 @@ public class Version6 extends JsonSchemaVersion{ public JsonMetaSchema getInstance() { return new JsonMetaSchema.Builder(URI) + .specification(SpecVersion.VersionFlag.V6) .idKeyword(ID) .addFormats(BUILTIN_FORMATS) .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V6)) diff --git a/src/main/java/com/networknt/schema/Version7.java b/src/main/java/com/networknt/schema/Version7.java index 51318fc69..e8b24efc8 100644 --- a/src/main/java/com/networknt/schema/Version7.java +++ b/src/main/java/com/networknt/schema/Version7.java @@ -13,6 +13,7 @@ public class Version7 extends JsonSchemaVersion{ @Override public JsonMetaSchema getInstance() { return new JsonMetaSchema.Builder(URI) + .specification(SpecVersion.VersionFlag.V7) .idKeyword(ID) .addFormats(BUILTIN_FORMATS) .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V7)) diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java index 5a33479fa..37bbd9527 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java @@ -216,7 +216,7 @@ private DynamicNode buildTest(JsonSchemaFactory validatorFactory, TestSpec testS } SchemaLocation testCaseFileUri = SchemaLocation.of("classpath:" + toForwardSlashPath(testSpec.getTestCase().getSpecification())); - JsonSchema schema = validatorFactory.getSchema(/*testCaseFileUri, */testSpec.getTestCase().getSchema(), config); + JsonSchema schema = validatorFactory.getSchema(testCaseFileUri, testSpec.getTestCase().getSchema(), config); return dynamicTest(testSpec.getDescription(), () -> executeAndReset(schema, testSpec)); } From b2482bc281ca77169106fb7060b8e8b2922b10e9 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 20:33:30 +0800 Subject: [PATCH 22/65] Support format assertion configuration --- .../com/networknt/schema/ExecutionConfig.java | 25 ++++++ .../com/networknt/schema/FormatValidator.java | 30 +++++--- .../com/networknt/schema/JsonMetaSchema.java | 4 +- .../format/BaseFormatJsonValidator.java | 48 ++++++++++++ .../schema/format/DateTimeValidator.java | 13 ++-- .../schema/AbstractJsonSchemaTestSuite.java | 7 +- .../schema/DurationFormatValidatorTest.java | 4 +- .../com/networknt/schema/Issue575Test.java | 4 +- .../schema/OverrideValidatorTest.java | 4 +- src/test/suite/tests/draft2020-12/format.json | 76 +++++-------------- 10 files changed, 136 insertions(+), 79 deletions(-) create mode 100644 src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java diff --git a/src/main/java/com/networknt/schema/ExecutionConfig.java b/src/main/java/com/networknt/schema/ExecutionConfig.java index 81ac03d0e..a7de8ed48 100644 --- a/src/main/java/com/networknt/schema/ExecutionConfig.java +++ b/src/main/java/com/networknt/schema/ExecutionConfig.java @@ -26,6 +26,7 @@ public class ExecutionConfig { private Locale locale = Locale.ROOT; private Predicate annotationAllowedPredicate = (keyword) -> true; + private Boolean formatAssertionsEnabled = null; public Locale getLocale() { return locale; @@ -84,4 +85,28 @@ public void setAnnotationAllowedPredicate(Predicate annotationAllowedPre this.annotationAllowedPredicate = Objects.requireNonNull(annotationAllowedPredicate, "annotationAllowedPredicate must not be null"); } + + /** + * Gets the format assertion enabled flag. + *

+ * This defaults to null meaning that it will follow the defaults of the + * specification. + *

+ * Since draft 2019-09 this will default to false unless enabled by using the + * $vocabulary keyword. + * + * @return the format assertions enabled flag + */ + public Boolean getFormatAssertionsEnabled() { + return formatAssertionsEnabled; + } + + /** + * Sets the format assertion enabled flag. + * + * @param formatAssertionsEnabled the format assertions enabled flag + */ + public void setFormatAssertionsEnabled(Boolean formatAssertionsEnabled) { + this.formatAssertionsEnabled = formatAssertionsEnabled; + } } diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java index 14d005780..cb3cb1f58 100644 --- a/src/main/java/com/networknt/schema/FormatValidator.java +++ b/src/main/java/com/networknt/schema/FormatValidator.java @@ -17,6 +17,8 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.format.BaseFormatJsonValidator; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,7 +27,7 @@ import java.util.Set; import java.util.regex.PatternSyntaxException; -public class FormatValidator extends BaseJsonValidator implements JsonValidator { +public class FormatValidator extends BaseFormatJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(FormatValidator.class); private final Format format; @@ -33,7 +35,6 @@ public class FormatValidator extends BaseJsonValidator implements JsonValidator public FormatValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, Format format, ValidatorTypeCode type) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, type, validationContext); this.format = format; - this.validationContext = validationContext; } public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { @@ -44,26 +45,33 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } + boolean assertionsEnabled = isAssertionsEnabled(executionContext); Set errors = new LinkedHashSet<>(); if (format != null) { if(format.getName().equals("ipv6")) { if(!node.textValue().trim().equals(node.textValue())) { - // leading and trailing spaces - errors.add(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()) - .arguments(format.getName(), format.getErrorMessageDescription()).build()); + if (assertionsEnabled) { + // leading and trailing spaces + errors.add(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .arguments(format.getName(), format.getErrorMessageDescription()).build()); + } } else if(node.textValue().contains("%")) { - // zone id is not part of the ipv6 - errors.add(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()) - .arguments(format.getName(), format.getErrorMessageDescription()).build()); + if (assertionsEnabled) { + // zone id is not part of the ipv6 + errors.add(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .arguments(format.getName(), format.getErrorMessageDescription()).build()); + } } } try { if (!format.matches(executionContext, node.textValue())) { - errors.add(message().instanceLocation(instanceLocation) + if (assertionsEnabled) { + errors.add(message().instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .arguments(format.getName(), format.getErrorMessageDescription()).build()); + } } } catch (PatternSyntaxException pse) { // String is considered valid if pattern is invalid diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index 0f2d8905a..c31706512 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -188,6 +188,7 @@ public JsonMetaSchema build() { this.idKeyword = idKeyword; this.keywords = keywords; this.specification = specification; + this.vocabularies = vocabularies; } public static JsonMetaSchema getV4() { @@ -236,12 +237,13 @@ public static Builder builder(String uri, JsonMetaSchema blueprint) { if (formatKeyword == null) { throw new IllegalArgumentException("The formatKeyword did not exist - blueprint is invalid."); } + Map vocabularies = new HashMap<>(blueprint.getVocabularies()); return builder(uri) .idKeyword(blueprint.idKeyword) .addKeywords(blueprint.keywords.values()) .addFormats(formatKeyword.getFormats()) .specification(blueprint.getSpecification()) - .vocabularies(blueprint.getVocabularies()) + .vocabularies(vocabularies) ; } diff --git a/src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java b/src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java new file mode 100644 index 000000000..948729502 --- /dev/null +++ b/src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java @@ -0,0 +1,48 @@ +package com.networknt.schema.format; + +import java.util.Map; + +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.BaseJsonValidator; +import com.networknt.schema.ExecutionContext; +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.ValidationContext; +import com.networknt.schema.ValidatorTypeCode; +import com.networknt.schema.SpecVersion.VersionFlag; + +public abstract class BaseFormatJsonValidator extends BaseJsonValidator { + protected final boolean assertionsEnabled; + + public BaseFormatJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidatorTypeCode validatorType, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, validatorType,validationContext); + VersionFlag specification = this.validationContext.getMetaSchema().getSpecification(); + if (specification == null || specification.getVersionFlagValue() < VersionFlag.V201909.getVersionFlagValue()) { + assertionsEnabled = true; + } else { + // Check vocabulary + assertionsEnabled = isFormatAssertionVocabularyEnabled(specification, + this.validationContext.getMetaSchema().getVocabularies()); + } + } + + protected boolean isFormatAssertionVocabularyEnabled(VersionFlag specification, Map vocabularies) { + if (VersionFlag.V202012.equals(specification)) { + String vocabulary = "https://json-schema.org/draft/2020-12/vocab/format-assertion"; + return vocabularies.containsKey(vocabulary); // doesn't matter if it is true or false + } else if (VersionFlag.V201909.equals(specification)) { + String vocabulary = "https://json-schema.org/draft/2019-09/vocab/format"; + return vocabularies.getOrDefault(vocabulary, false); + } + return false; + } + + protected boolean isAssertionsEnabled(ExecutionContext executionContext) { + if (Boolean.TRUE.equals(executionContext.getExecutionConfig().getFormatAssertionsEnabled())) { + return true; + } + return this.assertionsEnabled; + } +} diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index 5ac8352c9..d3daea685 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -19,7 +19,6 @@ import com.ethlo.time.ITU; import com.ethlo.time.LeapSecondException; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.BaseJsonValidator; import com.networknt.schema.ExecutionContext; import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; @@ -36,7 +35,7 @@ import java.util.Collections; import java.util.Set; -public class DateTimeValidator extends BaseJsonValidator { +public class DateTimeValidator extends BaseFormatJsonValidator { private static final Logger logger = LoggerFactory.getLogger(DateTimeValidator.class); private static final String DATETIME = "date-time"; @@ -53,10 +52,14 @@ public Set validate(ExecutionContext executionContext, JsonNo if (nodeType != JsonType.STRING) { return Collections.emptySet(); } + boolean assertionsEnabled = isAssertionsEnabled(executionContext); + if (!isLegalDateTime(node.textValue())) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(node.textValue(), DATETIME) - .build()); + if (assertionsEnabled) { + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(node.textValue(), DATETIME) + .build()); + } } return Collections.emptySet(); } diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java index 37bbd9527..66c471260 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java @@ -53,8 +53,11 @@ private static String toForwardSlashPath(Path file) { } private static void executeTest(JsonSchema schema, TestSpec testSpec) { - - Set errors = schema.validate(testSpec.getData()); + Set errors = schema.validate(testSpec.getData(), OutputFormat.DEFAULT, (executionContext, validationContext) -> { + if (testSpec.getTestCase().getSource().getPath().getParent().toString().endsWith("format")) { + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); + } + }); if (testSpec.isValid()) { if (!errors.isEmpty()) { diff --git a/src/test/java/com/networknt/schema/DurationFormatValidatorTest.java b/src/test/java/com/networknt/schema/DurationFormatValidatorTest.java index df85b3454..bcf80d342 100644 --- a/src/test/java/com/networknt/schema/DurationFormatValidatorTest.java +++ b/src/test/java/com/networknt/schema/DurationFormatValidatorTest.java @@ -41,7 +41,9 @@ public void durationFormatValidatorTest() throws JsonProcessingException, IOExce Set messages = validatorSchema.validate(validTargetNode); assertEquals(0, messages.size()); - messages = validatorSchema.validate(invalidTargetNode); + messages = validatorSchema.validate(invalidTargetNode, OutputFormat.DEFAULT, (executionContext, validationContext) -> { + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); + }); assertEquals(1, messages.size()); } diff --git a/src/test/java/com/networknt/schema/Issue575Test.java b/src/test/java/com/networknt/schema/Issue575Test.java index ff3c221a4..dd22d379c 100644 --- a/src/test/java/com/networknt/schema/Issue575Test.java +++ b/src/test/java/com/networknt/schema/Issue575Test.java @@ -121,7 +121,9 @@ public static Stream invalidTimeRepresentations() { @ParameterizedTest @MethodSource("invalidTimeRepresentations") void testInvalidTimeRepresentations(String jsonObject) throws JsonProcessingException { - Set errors = schema.validate(new ObjectMapper().readTree(jsonObject)); + Set errors = schema.validate(new ObjectMapper().readTree(jsonObject), OutputFormat.DEFAULT, (executionContext, validationContext) -> { + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); + }); Assertions.assertFalse(errors.isEmpty()); } } diff --git a/src/test/java/com/networknt/schema/OverrideValidatorTest.java b/src/test/java/com/networknt/schema/OverrideValidatorTest.java index 3c57f77c9..ccedb0ab9 100644 --- a/src/test/java/com/networknt/schema/OverrideValidatorTest.java +++ b/src/test/java/com/networknt/schema/OverrideValidatorTest.java @@ -48,7 +48,9 @@ public void overrideDefaultValidator() throws JsonProcessingException, IOExcepti final JsonSchemaFactory validatorFactory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).addMetaSchema(validatorMetaSchema).build(); final JsonSchema validatorSchema = validatorFactory.getSchema(schema); - Set messages = validatorSchema.validate(targetNode); + Set messages = validatorSchema.validate(targetNode, OutputFormat.DEFAULT, (executionContext, validationContext) -> { + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); + }); assertEquals(1, messages.size()); // Override EmailValidator diff --git a/src/test/suite/tests/draft2020-12/format.json b/src/test/suite/tests/draft2020-12/format.json index 18dba2755..01adcbda3 100644 --- a/src/test/suite/tests/draft2020-12/format.json +++ b/src/test/suite/tests/draft2020-12/format.json @@ -39,9 +39,7 @@ { "description": "invalid email string is only an annotation by default", "data": "2962", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -85,9 +83,7 @@ { "description": "invalid idn-email string is only an annotation by default", "data": "2962", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -131,9 +127,7 @@ { "description": "invalid regex string is only an annotation by default", "data": "^(abc]", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -177,9 +171,7 @@ { "description": "invalid ipv4 string is only an annotation by default", "data": "127.0.0.0.1", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -223,9 +215,7 @@ { "description": "invalid ipv6 string is only an annotation by default", "data": "12345::", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -269,9 +259,7 @@ { "description": "invalid idn-hostname string is only an annotation by default", "data": "〮실례.테스트", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -315,9 +303,7 @@ { "description": "invalid hostname string is only an annotation by default", "data": "-a-host-name-that-starts-with--", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -361,9 +347,7 @@ { "description": "invalid date string is only an annotation by default", "data": "06/19/1963", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -407,9 +391,7 @@ { "description": "invalid date-time string is only an annotation by default", "data": "1990-02-31T15:59:60.123-08:00", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -453,9 +435,7 @@ { "description": "invalid time string is only an annotation by default", "data": "08:30:06 PST", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -499,9 +479,7 @@ { "description": "invalid json-pointer string is only an annotation by default", "data": "/foo/bar~", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -545,9 +523,7 @@ { "description": "invalid relative-json-pointer string is only an annotation by default", "data": "/foo/bar", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -591,9 +567,7 @@ { "description": "invalid iri string is only an annotation by default", "data": "http://2001:0db8:85a3:0000:0000:8a2e:0370:7334", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -637,9 +611,7 @@ { "description": "invalid iri-reference string is only an annotation by default", "data": "\\\\WINDOWS\\filëßåré", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -683,9 +655,7 @@ { "description": "invalid uri string is only an annotation by default", "data": "//foo.bar/?baz=qux#quux", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -729,9 +699,7 @@ { "description": "invalid uri-reference string is only an annotation by default", "data": "\\\\WINDOWS\\fileshare", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -775,9 +743,7 @@ { "description": "invalid uri-template string is only an annotation by default", "data": "http://example.com/dictionary/{term:1}/{term", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -821,9 +787,7 @@ { "description": "invalid uuid string is only an annotation by default", "data": "2eb8aa08-aa98-11ea-b4aa-73b441d1638", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] }, @@ -867,9 +831,7 @@ { "description": "invalid duration string is only an annotation by default", "data": "PT1D", - "valid": true, - "disabled": true, - "reason": "TODO: Only the Format-Assertion vocabulary is currently supported" + "valid": true } ] } From 4f8112b6ad5320975366745e62386d99373e2ba6 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 22 Jan 2024 21:14:14 +0800 Subject: [PATCH 23/65] Refactor --- .../java/com/networknt/schema/JsonSchema.java | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 94a92848f..a2599daa2 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -26,6 +26,7 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.*; +import java.util.function.Consumer; /** * This is the core of json constraint implementation. It parses json constraint @@ -501,7 +502,7 @@ public Set validate(ExecutionContext executionContext, JsonNo /** * Validate the given root JsonNode, starting at the root of the data path. - * @param rootNode JsonNode + * @param rootNode the root node * * @return A list of ValidationMessage if there is any validation error, or an empty * list if there is no error. @@ -510,6 +511,26 @@ public Set validate(JsonNode rootNode) { return validate(rootNode, OutputFormat.DEFAULT); } + /** + * Validate the given root JsonNode, starting at the root of the data path. + * @param rootNode the root node + * @param executionCustomizer the execution customizer + * @return + */ + public Set validate(JsonNode rootNode, ExecutionCustomizer executionCustomizer) { + return validate(rootNode, OutputFormat.DEFAULT, executionCustomizer); + } + + /** + * Validate the given root JsonNode, starting at the root of the data path. + * @param rootNode the root node + * @param executionCustomizer the execution customizer + * @return + */ + public Set validate(JsonNode rootNode, Consumer executionCustomizer) { + return validate(rootNode, OutputFormat.DEFAULT, executionCustomizer); + } + /** * Validates the given root JsonNode, starting at the root of the data path. The * output will be formatted using the formatter specified. @@ -520,7 +541,7 @@ public Set validate(JsonNode rootNode) { * @return the result */ public T validate(JsonNode rootNode, OutputFormat format) { - return validate(rootNode, format, null); + return validate(rootNode, format, (ExecutionCustomizer) null); } /** @@ -528,7 +549,7 @@ public T validate(JsonNode rootNode, OutputFormat format) { * output will be formatted using the formatter specified. * * @param the result type - * @param rootNode the root note + * @param rootNode the root node * @param format the formatter * @param executionCustomizer the execution customizer * @return the result @@ -537,6 +558,22 @@ public T validate(JsonNode rootNode, OutputFormat format, ExecutionCustom return validate(createExecutionContext(), rootNode, format, executionCustomizer); } + /** + * Validates the given root JsonNode, starting at the root of the data path. The + * output will be formatted using the formatter specified. + * + * @param the result type + * @param rootNode the root node + * @param format the formatter + * @param executionCustomizer the execution customizer + * @return the result + */ + public T validate(JsonNode rootNode, OutputFormat format, Consumer executionCustomizer) { + return validate(createExecutionContext(), rootNode, format, (executionContext, validationContext) -> { + executionCustomizer.accept(executionContext); + }); + } + public ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode node) { return validateAndCollect(executionContext, node, node, atRoot()); } From 31faa6bd5f4094aa5679f3f370a9d82dac14395c Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 01:01:23 +0800 Subject: [PATCH 24/65] Refactor --- .../networknt/schema/JsonSchemaFactory.java | 2 +- .../schema/uri/ClasspathSchemaLoader.java | 8 +-- .../schema/uri/DefaultSchemaLoader.java | 25 ++++----- .../schema/uri/MapAbsoluteIriMapper.java | 4 +- .../schema/uri/PrefixAbsoluteIriMapper.java | 4 +- .../networknt/schema/uri/SchemaLoader.java | 4 +- .../schema/uri/SchemaLoaderBuilder.java | 12 ++--- .../networknt/schema/uri/SchemaLoaders.java | 50 ++++++++++++++++++ ...soluteIriMapper.java => SchemaMapper.java} | 2 +- .../networknt/schema/uri/SchemaMappers.java | 51 +++++++++++++++++++ .../networknt/schema/uri/UriSchemaLoader.java | 6 +-- .../com/networknt/schema/CustomUriTest.java | 2 +- .../schema/JsonSchemaFactoryUriCacheTest.java | 14 ++--- .../com/networknt/schema/UriMappingTest.java | 4 +- 14 files changed, 141 insertions(+), 47 deletions(-) create mode 100644 src/main/java/com/networknt/schema/uri/SchemaLoaders.java rename src/main/java/com/networknt/schema/uri/{AbsoluteIriMapper.java => SchemaMapper.java} (92%) create mode 100644 src/main/java/com/networknt/schema/uri/SchemaMappers.java diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index b443a5413..5f5e989c3 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -327,7 +327,7 @@ public JsonSchema getSchema(final SchemaLocation schemaUri, final SchemaValidato } protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValidatorsConfig config) { - try (InputStream inputStream = this.schemaLoader.getSchema(schemaUri).getInputStream()) { + try (InputStream inputStream = this.schemaLoader.getSchema(schemaUri.getAbsoluteIri()).getInputStream()) { if (inputStream == null) { throw new IOException("Cannot load schema at " + schemaUri.toString()); } diff --git a/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java b/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java index d357ba29f..8c7349bf9 100644 --- a/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java +++ b/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java @@ -17,7 +17,7 @@ import java.io.InputStream; -import com.networknt.schema.SchemaLocation; +import com.networknt.schema.AbsoluteIri; /** * Loads from classpath. @@ -25,15 +25,15 @@ public class ClasspathSchemaLoader implements SchemaLoader { @Override - public InputStreamSource getSchema(SchemaLocation schemaLocation) { - String scheme = schemaLocation.getAbsoluteIri().getScheme(); + public InputStreamSource getSchema(AbsoluteIri absoluteIri) { + String scheme = absoluteIri.getScheme(); if (scheme.startsWith("classpath") || scheme.startsWith("resource")) { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if (classLoader == null) { classLoader = SchemaLoader.class.getClassLoader(); } ClassLoader loader = classLoader; - String name = schemaLocation.getAbsoluteIri().toString().substring(scheme.length() + 1); + String name = absoluteIri.toString().substring(scheme.length() + 1); if (name.startsWith("//")) { name = name.substring(2); } diff --git a/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java b/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java index 5af7961ce..3ff132524 100644 --- a/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java +++ b/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java @@ -18,37 +18,30 @@ import java.util.List; import com.networknt.schema.AbsoluteIri; -import com.networknt.schema.SchemaLocation; /** * Default {@link SchemaLoader}. */ public class DefaultSchemaLoader implements SchemaLoader { private final List schemaLoaders; - private final List absoluteIriMappers; + private final List schemaMappers; - public DefaultSchemaLoader(List schemaLoaders, List absoluteIriMappers) { + public DefaultSchemaLoader(List schemaLoaders, List schemaMappers) { this.schemaLoaders = schemaLoaders; - this.absoluteIriMappers = absoluteIriMappers; + this.schemaMappers = schemaMappers; } @Override - public InputStreamSource getSchema(SchemaLocation schemaLocation) { - AbsoluteIri absoluteIri = schemaLocation.getAbsoluteIri(); - boolean modified = false; - SchemaLocation mappedSchemaLocation = schemaLocation; - for (AbsoluteIriMapper mapper : absoluteIriMappers) { - AbsoluteIri mapped = mapper.map(absoluteIri); + public InputStreamSource getSchema(AbsoluteIri absoluteIri) { + AbsoluteIri mappedResult = absoluteIri; + for (SchemaMapper mapper : schemaMappers) { + AbsoluteIri mapped = mapper.map(mappedResult); if (mapped != null) { - absoluteIri = mapped; - modified = true; + mappedResult = mapped; } } - if (modified) { - mappedSchemaLocation = new SchemaLocation(absoluteIri, schemaLocation.getFragment()); - } for (SchemaLoader loader : schemaLoaders) { - InputStreamSource result = loader.getSchema(mappedSchemaLocation); + InputStreamSource result = loader.getSchema(mappedResult); if (result != null) { return result; } diff --git a/src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java b/src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java index 3eaba98f5..53d443a92 100644 --- a/src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java +++ b/src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java @@ -5,9 +5,9 @@ import com.networknt.schema.AbsoluteIri; /** - * Map implementation of {@link AbsoluteIriMapper}. + * Map implementation of {@link SchemaMapper}. */ -public class MapAbsoluteIriMapper implements AbsoluteIriMapper { +public class MapAbsoluteIriMapper implements SchemaMapper { private final Map mappings; public MapAbsoluteIriMapper(Map mappings) { diff --git a/src/main/java/com/networknt/schema/uri/PrefixAbsoluteIriMapper.java b/src/main/java/com/networknt/schema/uri/PrefixAbsoluteIriMapper.java index e5cdc1708..7014a0d98 100644 --- a/src/main/java/com/networknt/schema/uri/PrefixAbsoluteIriMapper.java +++ b/src/main/java/com/networknt/schema/uri/PrefixAbsoluteIriMapper.java @@ -3,9 +3,9 @@ import com.networknt.schema.AbsoluteIri; /** - * Prefix implementation of {@link AbsoluteIriMapper}. + * Prefix implementation of {@link SchemaMapper}. */ -public class PrefixAbsoluteIriMapper implements AbsoluteIriMapper { +public class PrefixAbsoluteIriMapper implements SchemaMapper { private final String source; private final String replacement; diff --git a/src/main/java/com/networknt/schema/uri/SchemaLoader.java b/src/main/java/com/networknt/schema/uri/SchemaLoader.java index f9e27d5a5..8f1cc7fa5 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaLoader.java +++ b/src/main/java/com/networknt/schema/uri/SchemaLoader.java @@ -15,12 +15,12 @@ */ package com.networknt.schema.uri; -import com.networknt.schema.SchemaLocation; +import com.networknt.schema.AbsoluteIri; /** * Loader for schema. */ @FunctionalInterface public interface SchemaLoader { - InputStreamSource getSchema(SchemaLocation schemaLocation); + InputStreamSource getSchema(AbsoluteIri absoluteIri); } diff --git a/src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java b/src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java index 973e972c4..cd6d5a1a2 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java +++ b/src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java @@ -25,9 +25,9 @@ * Builder for {@link SchemaLoader}. */ public class SchemaLoaderBuilder { - private BiFunction, List, SchemaLoader> schemaLoaderFactory = DefaultSchemaLoader::new; + private BiFunction, List, SchemaLoader> schemaLoaderFactory = DefaultSchemaLoader::new; private List schemaLoaders = new ArrayList<>(); - private List absoluteIriMappers = new ArrayList<>(); + private List absoluteIriMappers = new ArrayList<>(); public SchemaLoaderBuilder() { this.schemaLoaders.add(new ClasspathSchemaLoader()); @@ -35,7 +35,7 @@ public SchemaLoaderBuilder() { } public SchemaLoaderBuilder schemaLoaderFactory( - BiFunction, List, SchemaLoader> schemaLoaderFactory) { + BiFunction, List, SchemaLoader> schemaLoaderFactory) { this.schemaLoaderFactory = schemaLoaderFactory; return this; } @@ -45,7 +45,7 @@ public SchemaLoaderBuilder schemaLoaders(List schemaLoaders) { return this; } - public SchemaLoaderBuilder absoluteIriMappers(List absoluteIriMappers) { + public SchemaLoaderBuilder absoluteIriMappers(List absoluteIriMappers) { this.absoluteIriMappers = absoluteIriMappers; return this; } @@ -55,12 +55,12 @@ public SchemaLoaderBuilder schemaLoaders(Consumer> schemaLoad return this; } - public SchemaLoaderBuilder absoluteIriMappers(Consumer> absoluteIriCustomizer) { + public SchemaLoaderBuilder absoluteIriMappers(Consumer> absoluteIriCustomizer) { absoluteIriCustomizer.accept(this.absoluteIriMappers); return this; } - public SchemaLoaderBuilder absoluteIriMapper(AbsoluteIriMapper absoluteIriMapper) { + public SchemaLoaderBuilder absoluteIriMapper(SchemaMapper absoluteIriMapper) { this.absoluteIriMappers.add(absoluteIriMapper); return this; } diff --git a/src/main/java/com/networknt/schema/uri/SchemaLoaders.java b/src/main/java/com/networknt/schema/uri/SchemaLoaders.java new file mode 100644 index 000000000..1160923f1 --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/SchemaLoaders.java @@ -0,0 +1,50 @@ +/* + * 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.uri; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Schema Loaders. + */ +public class SchemaLoaders extends ArrayList { + private static final long serialVersionUID = 1L; + + public SchemaLoaders() { + super(); + } + + public SchemaLoaders(Collection c) { + super(c); + } + + public SchemaLoaders(int initialCapacity) { + super(initialCapacity); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private SchemaLoaders values = new SchemaLoaders(); + + public SchemaLoaders build() { + return values; + } + } +} diff --git a/src/main/java/com/networknt/schema/uri/AbsoluteIriMapper.java b/src/main/java/com/networknt/schema/uri/SchemaMapper.java similarity index 92% rename from src/main/java/com/networknt/schema/uri/AbsoluteIriMapper.java rename to src/main/java/com/networknt/schema/uri/SchemaMapper.java index bed7d0dd1..914a61018 100644 --- a/src/main/java/com/networknt/schema/uri/AbsoluteIriMapper.java +++ b/src/main/java/com/networknt/schema/uri/SchemaMapper.java @@ -21,6 +21,6 @@ * Maps absolute IRI. */ @FunctionalInterface -public interface AbsoluteIriMapper { +public interface SchemaMapper { AbsoluteIri map(AbsoluteIri absoluteIRI); } diff --git a/src/main/java/com/networknt/schema/uri/SchemaMappers.java b/src/main/java/com/networknt/schema/uri/SchemaMappers.java new file mode 100644 index 000000000..c4afb6eaf --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/SchemaMappers.java @@ -0,0 +1,51 @@ +/* + * 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.uri; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Schema Mappers. + */ +public class SchemaMappers extends ArrayList { + private static final long serialVersionUID = 1L; + + public SchemaMappers() { + super(); + } + + public SchemaMappers(Collection c) { + super(c); + } + + public SchemaMappers(int initialCapacity) { + super(initialCapacity); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private SchemaMappers values = new SchemaMappers(); + + public SchemaMappers build() { + return values; + } + } + +} diff --git a/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java b/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java index bf69037df..863e7bc4c 100644 --- a/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java +++ b/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java @@ -17,15 +17,15 @@ import java.net.URI; -import com.networknt.schema.SchemaLocation; +import com.networknt.schema.AbsoluteIri; /** * Loads from uri. */ public class UriSchemaLoader implements SchemaLoader { @Override - public InputStreamSource getSchema(SchemaLocation schemaLocation) { - URI uri = URI.create(schemaLocation.getAbsoluteIri().toString()); + public InputStreamSource getSchema(AbsoluteIri absoluteIri) { + URI uri = URI.create(absoluteIri.toString()); return () -> uri.toURL().openStream(); } } diff --git a/src/test/java/com/networknt/schema/CustomUriTest.java b/src/test/java/com/networknt/schema/CustomUriTest.java index 9b35ef2fe..4f02a8db7 100644 --- a/src/test/java/com/networknt/schema/CustomUriTest.java +++ b/src/test/java/com/networknt/schema/CustomUriTest.java @@ -46,7 +46,7 @@ private static class CustomUriFetcher implements SchemaLoader { private static final String SCHEMA = "{\"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\"$id\":\"custom:date\",\"type\":\"string\",\"format\":\"date\"}"; @Override - public InputStreamSource getSchema(SchemaLocation schemaLocation) { + public InputStreamSource getSchema(AbsoluteIri absoluteIri) { return () -> new ByteArrayInputStream(SCHEMA.getBytes(StandardCharsets.UTF_8)); } } diff --git a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java index 71233d591..a2a5f5ed6 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java @@ -33,11 +33,11 @@ private void runCacheTest(boolean enableCache) throws JsonProcessingException { JsonSchemaFactory factory = buildJsonSchemaFactory(fetcher, enableCache); SchemaLocation schemaUri = SchemaLocation.of("cache:uri_mapping/schema1.json"); String schema = "{ \"$schema\": \"https://json-schema.org/draft/2020-12/schema#\", \"title\": \"json-object-with-schema\", \"type\": \"string\" }"; - fetcher.addResource(schemaUri, schema); + fetcher.addResource(schemaUri.getAbsoluteIri(), schema); assertEquals(objectMapper.readTree(schema), factory.getSchema(schemaUri, new SchemaValidatorsConfig()).schemaNode); String modifiedSchema = "{ \"$schema\": \"https://json-schema.org/draft/2020-12/schema#\", \"title\": \"json-object-with-schema\", \"type\": \"object\" }"; - fetcher.addResource(schemaUri, modifiedSchema); + fetcher.addResource(schemaUri.getAbsoluteIri(), modifiedSchema); assertEquals(objectMapper.readTree(enableCache ? schema : modifiedSchema), factory.getSchema(schemaUri, new SchemaValidatorsConfig()).schemaNode); } @@ -52,19 +52,19 @@ private JsonSchemaFactory buildJsonSchemaFactory(CustomURIFetcher uriFetcher, bo private class CustomURIFetcher implements SchemaLoader { - private Map uriToResource = new HashMap<>(); + private Map uriToResource = new HashMap<>(); - void addResource(SchemaLocation uri, String schema) { + void addResource(AbsoluteIri uri, String schema) { addResource(uri, new ByteArrayInputStream(schema.getBytes(StandardCharsets.UTF_8))); } - void addResource(SchemaLocation uri, InputStream is) { + void addResource(AbsoluteIri uri, InputStream is) { uriToResource.put(uri, is); } @Override - public InputStreamSource getSchema(SchemaLocation schemaLocation) { - return () -> uriToResource.get(schemaLocation); + public InputStreamSource getSchema(AbsoluteIri absoluteIri) { + return () -> uriToResource.get(absoluteIri); } } } diff --git a/src/test/java/com/networknt/schema/UriMappingTest.java b/src/test/java/com/networknt/schema/UriMappingTest.java index 1d916c8d4..be3b3b29b 100644 --- a/src/test/java/com/networknt/schema/UriMappingTest.java +++ b/src/test/java/com/networknt/schema/UriMappingTest.java @@ -18,7 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.JsonSchemaFactory.Builder; -import com.networknt.schema.uri.AbsoluteIriMapper; +import com.networknt.schema.uri.SchemaMapper; import com.networknt.schema.uri.MapAbsoluteIriMapper; import org.junit.jupiter.api.Test; @@ -155,7 +155,7 @@ public void testMappingsForRef() throws IOException { assertEquals(0, schema.validate(mapper.readTree("[]")).size()); } - private AbsoluteIriMapper getUriMappingsFromUrl(URL url) { + private SchemaMapper getUriMappingsFromUrl(URL url) { HashMap map = new HashMap(); try { for (JsonNode mapping : mapper.readTree(url)) { From 20312c37a820a7b5c3ce0d842d862fd44de120a9 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 05:29:16 +0800 Subject: [PATCH 25/65] Refactor --- .../networknt/schema/JsonSchemaFactory.java | 33 ++++--- .../schema/uri/MapAbsoluteIriMapper.java | 26 ------ .../networknt/schema/uri/MapSchemaLoader.java | 36 ++++++++ .../networknt/schema/uri/MapSchemaMapper.java | 31 +++++++ ...IriMapper.java => PrefixSchemaMapper.java} | 4 +- .../schema/uri/SchemaLoaderBuilder.java | 87 ------------------- .../networknt/schema/uri/SchemaLoaders.java | 62 ++++++++++++- .../networknt/schema/uri/SchemaMappers.java | 43 +++++++++ .../schema/AbstractJsonSchemaTestSuite.java | 2 +- .../com/networknt/schema/CustomUriTest.java | 9 +- .../com/networknt/schema/Issue285Test.java | 2 +- .../com/networknt/schema/Issue665Test.java | 4 +- .../com/networknt/schema/Issue824Test.java | 4 +- .../schema/JsonSchemaFactoryUriCacheTest.java | 2 +- .../com/networknt/schema/UriMappingTest.java | 14 +-- .../java/com/networknt/schema/UrnTest.java | 2 +- 16 files changed, 210 insertions(+), 151 deletions(-) delete mode 100644 src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java create mode 100644 src/main/java/com/networknt/schema/uri/MapSchemaLoader.java create mode 100644 src/main/java/com/networknt/schema/uri/MapSchemaMapper.java rename src/main/java/com/networknt/schema/uri/{PrefixAbsoluteIriMapper.java => PrefixSchemaMapper.java} (80%) delete mode 100644 src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 5f5e989c3..6895357cd 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -45,7 +45,8 @@ public static class Builder { private YAMLMapper yamlMapper = null; private String defaultMetaSchemaURI; private final ConcurrentMap jsonMetaSchemas = new ConcurrentHashMap(); - private SchemaLoaderBuilder schemaLoaderBuilder = new SchemaLoaderBuilder(); + private SchemaLoaders.Builder schemaLoadersBuilder = SchemaLoaders.builder(); + private SchemaMappers.Builder schemaMappersBuilder = SchemaMappers.builder(); private boolean enableUriSchemaCache = true; public Builder objectMapper(final ObjectMapper objectMapper) { @@ -80,8 +81,13 @@ public Builder enableUriSchemaCache(boolean enableUriSchemaCache) { return this; } - public Builder schemaLoaderBuilder(Consumer schemaLoaderBuilderCustomizer) { - schemaLoaderBuilderCustomizer.accept(this.schemaLoaderBuilder); + public Builder schemaLoaders(Consumer schemaLoadersBuilderCustomizer) { + schemaLoadersBuilderCustomizer.accept(this.schemaLoadersBuilder); + return this; + } + + public Builder schemaMappers(Consumer schemaMappersBuilderCustomizer) { + schemaMappersBuilderCustomizer.accept(this.schemaMappersBuilder); return this; } @@ -91,7 +97,8 @@ public JsonSchemaFactory build() { objectMapper == null ? new ObjectMapper() : objectMapper, yamlMapper == null ? new YAMLMapper(): yamlMapper, defaultMetaSchemaURI, - schemaLoaderBuilder, + schemaLoadersBuilder, + schemaMappersBuilder, jsonMetaSchemas, enableUriSchemaCache ); @@ -101,7 +108,8 @@ public JsonSchemaFactory build() { private final ObjectMapper jsonMapper; private final YAMLMapper yamlMapper; private final String defaultMetaSchemaURI; - private final SchemaLoaderBuilder schemaLoaderBuilder; + private final SchemaLoaders.Builder schemaLoadersBuilder; + private final SchemaMappers.Builder schemaMappersBuilder; private final SchemaLoader schemaLoader; private final Map jsonMetaSchemas; private final ConcurrentMap uriSchemaCache = new ConcurrentHashMap<>(); @@ -112,7 +120,8 @@ private JsonSchemaFactory( final ObjectMapper jsonMapper, final YAMLMapper yamlMapper, final String defaultMetaSchemaURI, - SchemaLoaderBuilder schemaLoaderBuilder, + SchemaLoaders.Builder schemaLoadersBuilder, + SchemaMappers.Builder schemaMappersBuilder, final Map jsonMetaSchemas, final boolean enableUriSchemaCache) { if (jsonMapper == null) { @@ -121,8 +130,10 @@ private JsonSchemaFactory( throw new IllegalArgumentException("YAMLMapper must not be null"); } else if (defaultMetaSchemaURI == null || defaultMetaSchemaURI.trim().isEmpty()) { throw new IllegalArgumentException("defaultMetaSchemaURI must not be null or empty"); - } else if (schemaLoaderBuilder == null) { + } else if (schemaLoadersBuilder == null) { throw new IllegalArgumentException("SchemaLoaders must not be null"); + } else if (schemaMappersBuilder == null) { + throw new IllegalArgumentException("SchemaMappers must not be null"); } else if (jsonMetaSchemas == null || jsonMetaSchemas.isEmpty()) { throw new IllegalArgumentException("Json Meta Schemas must not be null or empty"); } else if (jsonMetaSchemas.get(normalizeMetaSchemaUri(defaultMetaSchemaURI)) == null) { @@ -131,8 +142,9 @@ private JsonSchemaFactory( this.jsonMapper = jsonMapper; this.yamlMapper = yamlMapper; this.defaultMetaSchemaURI = defaultMetaSchemaURI; - this.schemaLoaderBuilder = schemaLoaderBuilder; - this.schemaLoader = schemaLoaderBuilder.build(); + this.schemaLoadersBuilder = schemaLoadersBuilder; + this.schemaMappersBuilder = schemaMappersBuilder; + this.schemaLoader = new DefaultSchemaLoader(schemaLoadersBuilder.build(), schemaMappersBuilder.build()); this.jsonMetaSchemas = jsonMetaSchemas; this.enableUriSchemaCache = enableUriSchemaCache; } @@ -193,7 +205,8 @@ public static Builder builder(final JsonSchemaFactory blueprint) { .defaultMetaSchemaURI(blueprint.defaultMetaSchemaURI) .objectMapper(blueprint.jsonMapper) .yamlMapper(blueprint.yamlMapper); - builder.schemaLoaderBuilder = blueprint.schemaLoaderBuilder; + builder.schemaLoadersBuilder.with(blueprint.schemaLoadersBuilder); + builder.schemaMappersBuilder.with(blueprint.schemaMappersBuilder); return builder; } diff --git a/src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java b/src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java deleted file mode 100644 index 53d443a92..000000000 --- a/src/main/java/com/networknt/schema/uri/MapAbsoluteIriMapper.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.networknt.schema.uri; - -import java.util.Map; - -import com.networknt.schema.AbsoluteIri; - -/** - * Map implementation of {@link SchemaMapper}. - */ -public class MapAbsoluteIriMapper implements SchemaMapper { - private final Map mappings; - - public MapAbsoluteIriMapper(Map mappings) { - this.mappings = mappings; - } - - @Override - public AbsoluteIri map(AbsoluteIri absoluteIRI) { - String mapped = this.mappings.get(absoluteIRI.toString()); - if (mapped != null) { - return AbsoluteIri.of(mapped); - } - return null; - } - -} diff --git a/src/main/java/com/networknt/schema/uri/MapSchemaLoader.java b/src/main/java/com/networknt/schema/uri/MapSchemaLoader.java new file mode 100644 index 000000000..d6b036d19 --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/MapSchemaLoader.java @@ -0,0 +1,36 @@ +package com.networknt.schema.uri; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.function.Function; + +import com.networknt.schema.AbsoluteIri; + +/** + * Map implementation of {@link SchemaLoader}. + */ +public class MapSchemaLoader implements SchemaLoader { + private final Function mappings; + + public MapSchemaLoader(Map mappings) { + this(mappings::get); + } + + public MapSchemaLoader(Function mappings) { + this.mappings = mappings; + } + + @Override + public InputStreamSource getSchema(AbsoluteIri absoluteIri) { + try { + String result = mappings.apply(absoluteIri.toString()); + if (result != null) { + return () -> new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8)); + } + } catch (Exception e) { + // Do nothing + } + return null; + } +} diff --git a/src/main/java/com/networknt/schema/uri/MapSchemaMapper.java b/src/main/java/com/networknt/schema/uri/MapSchemaMapper.java new file mode 100644 index 000000000..25916374b --- /dev/null +++ b/src/main/java/com/networknt/schema/uri/MapSchemaMapper.java @@ -0,0 +1,31 @@ +package com.networknt.schema.uri; + +import java.util.Map; +import java.util.function.Function; + +import com.networknt.schema.AbsoluteIri; + +/** + * Map implementation of {@link SchemaMapper}. + */ +public class MapSchemaMapper implements SchemaMapper { + private final Function mappings; + + public MapSchemaMapper(Map mappings) { + this(mappings::get); + } + + public MapSchemaMapper(Function mappings) { + this.mappings = mappings; + } + + @Override + public AbsoluteIri map(AbsoluteIri absoluteIRI) { + String mapped = this.mappings.apply(absoluteIRI.toString()); + if (mapped != null) { + return AbsoluteIri.of(mapped); + } + return null; + } + +} diff --git a/src/main/java/com/networknt/schema/uri/PrefixAbsoluteIriMapper.java b/src/main/java/com/networknt/schema/uri/PrefixSchemaMapper.java similarity index 80% rename from src/main/java/com/networknt/schema/uri/PrefixAbsoluteIriMapper.java rename to src/main/java/com/networknt/schema/uri/PrefixSchemaMapper.java index 7014a0d98..aa7eee080 100644 --- a/src/main/java/com/networknt/schema/uri/PrefixAbsoluteIriMapper.java +++ b/src/main/java/com/networknt/schema/uri/PrefixSchemaMapper.java @@ -5,11 +5,11 @@ /** * Prefix implementation of {@link SchemaMapper}. */ -public class PrefixAbsoluteIriMapper implements SchemaMapper { +public class PrefixSchemaMapper implements SchemaMapper { private final String source; private final String replacement; - public PrefixAbsoluteIriMapper(String source, String replacement) { + public PrefixSchemaMapper(String source, String replacement) { this.source = source; this.replacement = replacement; } diff --git a/src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java b/src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java deleted file mode 100644 index cd6d5a1a2..000000000 --- a/src/main/java/com/networknt/schema/uri/SchemaLoaderBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2023 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.uri; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.function.BiFunction; -import java.util.function.Consumer; - -/** - * Builder for {@link SchemaLoader}. - */ -public class SchemaLoaderBuilder { - private BiFunction, List, SchemaLoader> schemaLoaderFactory = DefaultSchemaLoader::new; - private List schemaLoaders = new ArrayList<>(); - private List absoluteIriMappers = new ArrayList<>(); - - public SchemaLoaderBuilder() { - this.schemaLoaders.add(new ClasspathSchemaLoader()); - this.schemaLoaders.add(new UriSchemaLoader()); - } - - public SchemaLoaderBuilder schemaLoaderFactory( - BiFunction, List, SchemaLoader> schemaLoaderFactory) { - this.schemaLoaderFactory = schemaLoaderFactory; - return this; - } - - public SchemaLoaderBuilder schemaLoaders(List schemaLoaders) { - this.schemaLoaders = schemaLoaders; - return this; - } - - public SchemaLoaderBuilder absoluteIriMappers(List absoluteIriMappers) { - this.absoluteIriMappers = absoluteIriMappers; - return this; - } - - public SchemaLoaderBuilder schemaLoaders(Consumer> schemaLoaderCustomizer) { - schemaLoaderCustomizer.accept(this.schemaLoaders); - return this; - } - - public SchemaLoaderBuilder absoluteIriMappers(Consumer> absoluteIriCustomizer) { - absoluteIriCustomizer.accept(this.absoluteIriMappers); - return this; - } - - public SchemaLoaderBuilder absoluteIriMapper(SchemaMapper absoluteIriMapper) { - this.absoluteIriMappers.add(absoluteIriMapper); - return this; - } - - public SchemaLoaderBuilder schemaLoader(SchemaLoader schemaLoader) { - this.schemaLoaders.add(0, schemaLoader); - return this; - } - - public SchemaLoaderBuilder mapPrefix(String source, String replacement) { - this.absoluteIriMappers.add(new PrefixAbsoluteIriMapper(source, replacement)); - return this; - } - - public SchemaLoaderBuilder map(Map mappings) { - this.absoluteIriMappers.add(new MapAbsoluteIriMapper(mappings)); - return this; - } - - public SchemaLoader build() { - return schemaLoaderFactory.apply(this.schemaLoaders, this.absoluteIriMappers); - } - -} diff --git a/src/main/java/com/networknt/schema/uri/SchemaLoaders.java b/src/main/java/com/networknt/schema/uri/SchemaLoaders.java index 1160923f1..a433daac9 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaLoaders.java +++ b/src/main/java/com/networknt/schema/uri/SchemaLoaders.java @@ -17,6 +17,10 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; /** * Schema Loaders. @@ -24,6 +28,17 @@ public class SchemaLoaders extends ArrayList { private static final long serialVersionUID = 1L; + private static final ClasspathSchemaLoader CLASSPATH_SCHEMA_LOADER = new ClasspathSchemaLoader(); + private static final UriSchemaLoader URI_SCHEMA_LOADER = new UriSchemaLoader(); + private static final SchemaLoaders DEFAULT; + + static { + SchemaLoaders schemaLoaders = new SchemaLoaders(); + schemaLoaders.add(CLASSPATH_SCHEMA_LOADER); + schemaLoaders.add(URI_SCHEMA_LOADER); + DEFAULT = schemaLoaders; + } + public SchemaLoaders() { super(); } @@ -35,16 +50,57 @@ public SchemaLoaders(Collection c) { public SchemaLoaders(int initialCapacity) { super(initialCapacity); } - + public static Builder builder() { return new Builder(); } public static class Builder { - private SchemaLoaders values = new SchemaLoaders(); + SchemaLoaders values = new SchemaLoaders(); + + public Builder() { + } + + public Builder(Builder copy) { + this.values.addAll(copy.values); + } + + public Builder with(Builder builder) { + if (!builder.values.isEmpty()) { + this.values.addAll(builder.values); + } + return this; + } + + public Builder values(Consumer> values) { + values.accept(this.values); + return this; + } + public Builder add(SchemaLoader schemaLoader) { + this.values.add(schemaLoader); + return this; + } + + public Builder values(Map mappings) { + this.values.add(new MapSchemaLoader(mappings)); + return this; + } + + public Builder values(Function mappings) { + this.values.add(new MapSchemaLoader(mappings)); + return this; + } + public SchemaLoaders build() { - return values; + if (this.values.isEmpty()) { + return DEFAULT; + } + SchemaLoaders result = new SchemaLoaders(); + result.add(CLASSPATH_SCHEMA_LOADER); + result.addAll(this.values); + result.add(URI_SCHEMA_LOADER); + return result; } } } diff --git a/src/main/java/com/networknt/schema/uri/SchemaMappers.java b/src/main/java/com/networknt/schema/uri/SchemaMappers.java index c4afb6eaf..1a21abdab 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaMappers.java +++ b/src/main/java/com/networknt/schema/uri/SchemaMappers.java @@ -17,6 +17,10 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; /** * Schema Mappers. @@ -42,7 +46,46 @@ public static Builder builder() { public static class Builder { private SchemaMappers values = new SchemaMappers(); + + public Builder() { + } + + public Builder(Builder copy) { + this.values.addAll(copy.values); + } + + public Builder with(Builder builder) { + if (!builder.values.isEmpty()) { + this.values.addAll(builder.values); + } + return this; + } + + public Builder values(Consumer> values) { + values.accept(this.values); + return this; + } + + public Builder add(SchemaMapper schemaMapper) { + this.values.add(schemaMapper); + return this; + } + + public Builder mapPrefix(String source, String replacement) { + this.values.add(new PrefixSchemaMapper(source, replacement)); + return this; + } + + public Builder values(Map mappings) { + this.values.add(new MapSchemaMapper(mappings)); + return this; + } + public Builder values(Function mappings) { + this.values.add(new MapSchemaMapper(mappings)); + return this; + } + public SchemaMappers build() { return values; } diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java index 66c471260..f318f164f 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java @@ -186,7 +186,7 @@ private JsonSchemaFactory buildValidatorFactory(VersionFlag defaultVersion, Test return JsonSchemaFactory .builder(base) .objectMapper(this.mapper) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder + .schemaMappers(schemaMappers -> schemaMappers .mapPrefix("https://", "http://") .mapPrefix("http://json-schema.org", "resource:")) .build(); diff --git a/src/test/java/com/networknt/schema/CustomUriTest.java b/src/test/java/com/networknt/schema/CustomUriTest.java index 4f02a8db7..d796887be 100644 --- a/src/test/java/com/networknt/schema/CustomUriTest.java +++ b/src/test/java/com/networknt/schema/CustomUriTest.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.uri.InputStreamSource; import com.networknt.schema.uri.SchemaLoader; -import com.networknt.schema.uri.UriSchemaLoader; import org.junit.jupiter.api.Test; @@ -33,13 +32,7 @@ public void customUri() throws Exception { private JsonSchemaFactory buildJsonSchemaFactory() { return JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.schemaLoaders(schemaLoaders -> { - for (int x = 0; x < schemaLoaders.size(); x++) { - if (schemaLoaders.get(x) instanceof UriSchemaLoader) { - schemaLoaders.set(x, new CustomUriFetcher()); - } - } - })).build(); + .schemaLoaders(schemaLoaders -> schemaLoaders.add(new CustomUriFetcher())).build(); } private static class CustomUriFetcher implements SchemaLoader { diff --git a/src/test/java/com/networknt/schema/Issue285Test.java b/src/test/java/com/networknt/schema/Issue285Test.java index e64f8b673..ff868ccae 100644 --- a/src/test/java/com/networknt/schema/Issue285Test.java +++ b/src/test/java/com/networknt/schema/Issue285Test.java @@ -16,7 +16,7 @@ public class Issue285Test { private JsonSchemaFactory schemaFactory = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)) .objectMapper(mapper) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder + .schemaMappers(schemaMappers -> schemaMappers .mapPrefix("http://json-schema.org", "resource:") .mapPrefix("https://json-schema.org", "resource:")) .build(); diff --git a/src/test/java/com/networknt/schema/Issue665Test.java b/src/test/java/com/networknt/schema/Issue665Test.java index 1d57e6f7d..2973cc4f1 100644 --- a/src/test/java/com/networknt/schema/Issue665Test.java +++ b/src/test/java/com/networknt/schema/Issue665Test.java @@ -25,8 +25,8 @@ void testUrnUriAsLocalRef() throws IOException { void testUrnUriAsLocalRef_ExternalURN() { JsonSchemaFactory factory = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)) - .schemaLoaderBuilder(schemaLoaderBuilder -> { - schemaLoaderBuilder.map(Collections.singletonMap("urn:data", + .schemaMappers(schemaMappers -> { + schemaMappers.values(Collections.singletonMap("urn:data", "classpath:draft7/urn/issue665_external_urn_subschema.json")); }) .build(); diff --git a/src/test/java/com/networknt/schema/Issue824Test.java b/src/test/java/com/networknt/schema/Issue824Test.java index bcb2229b4..dc3279c78 100644 --- a/src/test/java/com/networknt/schema/Issue824Test.java +++ b/src/test/java/com/networknt/schema/Issue824Test.java @@ -15,8 +15,8 @@ public class Issue824Test { void validate() throws JsonProcessingException { final JsonSchema v201909SpecSchema = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)) - .schemaLoaderBuilder(schemaLoaderBuilder -> { - schemaLoaderBuilder.mapPrefix("https://json-schema.org", "resource:"); + .schemaMappers(schemaMappers -> { + schemaMappers.mapPrefix("https://json-schema.org", "resource:"); }).build() .getSchema(SchemaLocation.of(JsonMetaSchema.getV201909().getUri())); v201909SpecSchema.preloadJsonSchema(); diff --git a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java index a2a5f5ed6..f6660f2d3 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java @@ -45,7 +45,7 @@ private void runCacheTest(boolean enableCache) throws JsonProcessingException { private JsonSchemaFactory buildJsonSchemaFactory(CustomURIFetcher uriFetcher, boolean enableUriSchemaCache) { return JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)) .enableUriSchemaCache(enableUriSchemaCache) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.schemaLoader(uriFetcher)) + .schemaLoaders(schemaLoaders -> schemaLoaders.add(uriFetcher)) .addMetaSchema(JsonMetaSchema.getV202012()) .build(); } diff --git a/src/test/java/com/networknt/schema/UriMappingTest.java b/src/test/java/com/networknt/schema/UriMappingTest.java index be3b3b29b..c6ac2286a 100644 --- a/src/test/java/com/networknt/schema/UriMappingTest.java +++ b/src/test/java/com/networknt/schema/UriMappingTest.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.JsonSchemaFactory.Builder; import com.networknt.schema.uri.SchemaMapper; -import com.networknt.schema.uri.MapAbsoluteIriMapper; +import com.networknt.schema.uri.MapSchemaMapper; import org.junit.jupiter.api.Test; import java.io.FileNotFoundException; @@ -49,7 +49,7 @@ public void testBuilderUriMappingUri() throws IOException { Builder builder = JsonSchemaFactory.builder() .defaultMetaSchemaURI(draftV4.getUri()) .addMetaSchema(draftV4) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))); + .schemaMappers(schemaMappers -> schemaMappers.add(getUriMappingsFromUrl(mappings))); JsonSchemaFactory instance = builder.build(); JsonSchema schema = instance.getSchema(SchemaLocation.of( "https://raw.githubusercontent.com/networknt/json-schema-validator/master/src/test/resources/draft4/extra/uri_mapping/uri-mapping.schema.json")); @@ -87,7 +87,7 @@ public void testBuilderExampleMappings() throws IOException { Builder builder = JsonSchemaFactory.builder() .defaultMetaSchemaURI(draftV4.getUri()) .addMetaSchema(draftV4) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))); + .schemaMappers(schemaMappers -> schemaMappers.add(getUriMappingsFromUrl(mappings))); instance = builder.build(); JsonSchema schema = instance.getSchema(example); assertEquals(0, schema.validate(mapper.createObjectNode()).size()); @@ -103,7 +103,7 @@ public void testBuilderExampleMappings() throws IOException { public void testValidatorConfigUriMappingUri() throws IOException { URL mappings = UriMappingTest.class.getResource("/draft4/extra/uri_mapping/uri-mapping.json"); JsonSchemaFactory instance = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))).build(); + .schemaMappers(schemaMappers -> schemaMappers.add(getUriMappingsFromUrl(mappings))).build(); JsonSchema schema = instance.getSchema(SchemaLocation.of( "https://raw.githubusercontent.com/networknt/json-schema-validator/master/src/test/resources/draft4/extra/uri_mapping/uri-mapping.schema.json")); assertEquals(0, schema.validate(mapper.readTree(mappings)).size()); @@ -139,7 +139,7 @@ public void testValidatorConfigExampleMappings() throws IOException { fail("Unexpected exception thrown"); } instance = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))).build(); + .schemaMappers(schemaMappers -> schemaMappers.add(getUriMappingsFromUrl(mappings))).build(); JsonSchema schema = instance.getSchema(example, config); assertEquals(0, schema.validate(mapper.createObjectNode()).size()); } @@ -148,7 +148,7 @@ public void testValidatorConfigExampleMappings() throws IOException { public void testMappingsForRef() throws IOException { URL mappings = UriMappingTest.class.getResource("/draft4/extra/uri_mapping/schema-with-ref-mapping.json"); JsonSchemaFactory instance = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(getUriMappingsFromUrl(mappings))).build(); + .schemaMappers(schemaMappers -> schemaMappers.add(getUriMappingsFromUrl(mappings))).build(); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); JsonSchema schema = instance.getSchema(SchemaLocation.of("resource:draft4/extra/uri_mapping/schema-with-ref.json"), config); @@ -165,6 +165,6 @@ private SchemaMapper getUriMappingsFromUrl(URL url) { } catch (IOException e) { throw new UncheckedIOException(e); } - return new MapAbsoluteIriMapper(map); + return new MapSchemaMapper(map); } } diff --git a/src/test/java/com/networknt/schema/UrnTest.java b/src/test/java/com/networknt/schema/UrnTest.java index 02c4d03db..77ef35f25 100644 --- a/src/test/java/com/networknt/schema/UrnTest.java +++ b/src/test/java/com/networknt/schema/UrnTest.java @@ -29,7 +29,7 @@ public void testURNToURI() throws Exception { JsonSchemaFactory.Builder builder = JsonSchemaFactory.builder() .defaultMetaSchemaURI(draftV7.getUri()) .addMetaSchema(draftV7) - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.absoluteIriMapper(value -> AbsoluteIri.of(String.format("resource:draft7/urn/%s.schema.json", value.toString()))) + .schemaMappers(schemaMappers -> schemaMappers.add(value -> AbsoluteIri.of(String.format("resource:draft7/urn/%s.schema.json", value.toString()))) ); JsonSchemaFactory instance = builder.build(); JsonSchema schema = instance.getSchema(is); From ff14724ccf0e629760e5e5eec4375ea7764b952b Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 08:40:49 +0800 Subject: [PATCH 26/65] Update docs --- doc/schema-retrieval.md | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/doc/schema-retrieval.md b/doc/schema-retrieval.md index 08e37f480..e253699b7 100644 --- a/doc/schema-retrieval.md +++ b/doc/schema-retrieval.md @@ -8,14 +8,14 @@ In the event that the schema does not define a schema identifier using the `$id` ## Mapping Schema Identifier to Retrieval IRI -The schema identifier can be mapped to the retrieval IRI by implementing the `AbsoluteIriMapper` interface. +The schema identifier can be mapped to the retrieval IRI by implementing the `SchemaMapper` interface. -### Configuring AbsoluteIriMapper +### Configuring Schema Mapper ```java JsonSchemaFactory schemaFactory = JsonSchemaFactory.builder() - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder - .absoluteIriMapper(new CustomAbsoluteIriMapper()) + .schemaMappers(schemaMappers -> schemaMappers + .add(new CustomSchemaMapper()) .addMetaSchema(JsonMetaSchema.getV7()) .defaultMetaSchemaURI(JsonMetaSchema.getV7().getUri()) .build(); @@ -25,7 +25,7 @@ JsonSchemaFactory schemaFactory = JsonSchemaFactory.builder() ```java JsonSchemaFactory schemaFactory = JsonSchemaFactory.builder() - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder + .schemaMappers(schemaMappers -> schemaMappers .mapPrefix("https://", "http://") .mapPrefix("http://json-schema.org", "classpath:")) .addMetaSchema(JsonMetaSchema.getV7()) @@ -58,8 +58,8 @@ public class CustomUriSchemaLoader implements SchemaLoader { } @Override - public InputStreamSource getSchema(SchemaLocation schemaLocation) { - URI uri = URI.create(schemaLocation.getAbsoluteIri().toString()); + public InputStreamSource getSchema(AbsoluteIri absoluteIri) { + URI uri = URI.create(absoluteIri.toString()); return () -> { HttpRequest request = HttpRequest.newBuilder().uri(uri).header("Authorization", authorizationToken).build(); try { @@ -83,12 +83,7 @@ Within the `JsonSchemaFactory` the custom `SchemaLoader` must be configured. CustomUriSchemaLoader uriSchemaLoader = new CustomUriSchemaLoader(authorizationToken); JsonSchemaFactory schemaFactory = JsonSchemaFactory.builder() - .schemaLoaderBuilder(schemaLoaderBuilder -> schemaLoaderBuilder.schemaLoaders(schemaLoaders -> { - for (int x = 0; x < schemaLoaders.size(); x++) { - if (schemaLoaders.get(x) instanceof UriSchemaLoader) { - schemaLoaders.set(x, uriSchemaLoader); - } - } + .schemaLoaders(schemaLoaders -> schemaLoaders.add(uriSchemaLoader)) .addMetaSchema(JsonMetaSchema.getV7()) .defaultMetaSchemaURI(JsonMetaSchema.getV7().getUri()) .build(); From b277106efa526decb76d23106b8627bc8e4057b0 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 09:05:55 +0800 Subject: [PATCH 27/65] Allow yaml to be optional --- .../networknt/schema/JsonMapperFactory.java | 36 +++++++ .../networknt/schema/JsonSchemaFactory.java | 96 +++++++++++++++---- .../networknt/schema/YamlMapperFactory.java | 36 +++++++ .../schema/AbstractJsonSchemaTest.java | 2 +- .../schema/AbstractJsonSchemaTestSuite.java | 4 +- .../schema/BaseJsonSchemaValidatorTest.java | 2 +- .../com/networknt/schema/Issue285Test.java | 2 +- .../schema/Issue366FailFastTest.java | 2 +- .../schema/Issue366FailSlowTest.java | 2 +- .../com/networknt/schema/Issue428Test.java | 2 +- .../com/networknt/schema/Issue510Test.java | 2 +- .../schema/OpenAPI30JsonSchemaTest.java | 2 +- .../schema/UnknownMetaSchemaTest.java | 6 +- .../networknt/schema/V4JsonSchemaTest.java | 2 +- .../networknt/schema/suite/TestSource.java | 3 +- 15 files changed, 164 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/networknt/schema/JsonMapperFactory.java create mode 100644 src/main/java/com/networknt/schema/YamlMapperFactory.java diff --git a/src/main/java/com/networknt/schema/JsonMapperFactory.java b/src/main/java/com/networknt/schema/JsonMapperFactory.java new file mode 100644 index 000000000..d6edf05b2 --- /dev/null +++ b/src/main/java/com/networknt/schema/JsonMapperFactory.java @@ -0,0 +1,36 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; + +/** + * Json Mapper Factory. + */ +public class JsonMapperFactory { + + /** + * The holder defers the classloading until it is used. + */ + private static class Holder { + private static final ObjectMapper INSTANCE = JsonMapper.builder().build(); + } + + public static ObjectMapper getInstance() { + return Holder.INSTANCE; + } +} diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 6895357cd..fb5bb41b5 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.uri.*; import org.slf4j.Logger; @@ -41,20 +40,20 @@ public class JsonSchemaFactory { public static class Builder { - private ObjectMapper objectMapper = null; - private YAMLMapper yamlMapper = null; + private ObjectMapper jsonMapper = null; + private ObjectMapper yamlMapper = null; private String defaultMetaSchemaURI; private final ConcurrentMap jsonMetaSchemas = new ConcurrentHashMap(); private SchemaLoaders.Builder schemaLoadersBuilder = SchemaLoaders.builder(); private SchemaMappers.Builder schemaMappersBuilder = SchemaMappers.builder(); private boolean enableUriSchemaCache = true; - public Builder objectMapper(final ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + public Builder jsonMapper(final ObjectMapper jsonMapper) { + this.jsonMapper = jsonMapper; return this; } - public Builder yamlMapper(final YAMLMapper yamlMapper) { + public Builder yamlMapper(final ObjectMapper yamlMapper) { this.yamlMapper = yamlMapper; return this; } @@ -94,8 +93,8 @@ public Builder schemaMappers(Consumer schemaMappersBuilde public JsonSchemaFactory build() { // create builtin keywords with (custom) formats. return new JsonSchemaFactory( - objectMapper == null ? new ObjectMapper() : objectMapper, - yamlMapper == null ? new YAMLMapper(): yamlMapper, + jsonMapper, + yamlMapper, defaultMetaSchemaURI, schemaLoadersBuilder, schemaMappersBuilder, @@ -106,7 +105,7 @@ public JsonSchemaFactory build() { } private final ObjectMapper jsonMapper; - private final YAMLMapper yamlMapper; + private final ObjectMapper yamlMapper; private final String defaultMetaSchemaURI; private final SchemaLoaders.Builder schemaLoadersBuilder; private final SchemaMappers.Builder schemaMappersBuilder; @@ -118,17 +117,13 @@ public JsonSchemaFactory build() { private JsonSchemaFactory( final ObjectMapper jsonMapper, - final YAMLMapper yamlMapper, + final ObjectMapper yamlMapper, final String defaultMetaSchemaURI, SchemaLoaders.Builder schemaLoadersBuilder, SchemaMappers.Builder schemaMappersBuilder, final Map jsonMetaSchemas, final boolean enableUriSchemaCache) { - if (jsonMapper == null) { - throw new IllegalArgumentException("ObjectMapper must not be null"); - } else if (yamlMapper == null) { - throw new IllegalArgumentException("YAMLMapper must not be null"); - } else if (defaultMetaSchemaURI == null || defaultMetaSchemaURI.trim().isEmpty()) { + if (defaultMetaSchemaURI == null || defaultMetaSchemaURI.trim().isEmpty()) { throw new IllegalArgumentException("defaultMetaSchemaURI must not be null or empty"); } else if (schemaLoadersBuilder == null) { throw new IllegalArgumentException("SchemaLoaders must not be null"); @@ -203,7 +198,7 @@ public static Builder builder(final JsonSchemaFactory blueprint) { Builder builder = builder() .addMetaSchemas(blueprint.jsonMetaSchemas.values()) .defaultMetaSchemaURI(blueprint.defaultMetaSchemaURI) - .objectMapper(blueprint.jsonMapper) + .jsonMapper(blueprint.jsonMapper) .yamlMapper(blueprint.yamlMapper); builder.schemaLoadersBuilder.with(blueprint.schemaLoadersBuilder); builder.schemaMappersBuilder.with(blueprint.schemaMappersBuilder); @@ -303,7 +298,7 @@ private JsonMetaSchema fromId(String id, SchemaValidatorsConfig config) { public JsonSchema getSchema(final String schema, final SchemaValidatorsConfig config) { try { - final JsonNode schemaNode = jsonMapper.readTree(schema); + final JsonNode schemaNode = getJsonMapper().readTree(schema); return newJsonSchema(null, schemaNode, config); } catch (IOException ioe) { logger.error("Failed to load json schema!", ioe); @@ -317,7 +312,7 @@ public JsonSchema getSchema(final String schema) { public JsonSchema getSchema(final InputStream schemaStream, final SchemaValidatorsConfig config) { try { - final JsonNode schemaNode = jsonMapper.readTree(schemaStream); + final JsonNode schemaNode = getJsonMapper().readTree(schemaStream); return newJsonSchema(null, schemaNode, config); } catch (IOException ioe) { logger.error("Failed to load json schema!", ioe); @@ -329,6 +324,13 @@ public JsonSchema getSchema(final InputStream schemaStream) { return getSchema(schemaStream, null); } + /** + * Gets the schema. + * + * @param schemaUri the absolute IRI of the schema which can map to the retrieval IRI. + * @param config the config + * @return the schema + */ public JsonSchema getSchema(final SchemaLocation schemaUri, final SchemaValidatorsConfig config) { if (enableUriSchemaCache) { JsonSchema cachedUriSchema = uriSchemaCache.computeIfAbsent(schemaUri, key -> { @@ -339,6 +341,14 @@ public JsonSchema getSchema(final SchemaLocation schemaUri, final SchemaValidato return getMappedSchema(schemaUri, config); } + protected ObjectMapper getYamlMapper() { + return this.yamlMapper != null ? this.yamlMapper : YamlMapperFactory.getInstance(); + } + + protected ObjectMapper getJsonMapper() { + return this.jsonMapper != null ? this.jsonMapper : JsonMapperFactory.getInstance(); + } + protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValidatorsConfig config) { try (InputStream inputStream = this.schemaLoader.getSchema(schemaUri.getAbsoluteIri()).getInputStream()) { if (inputStream == null) { @@ -346,9 +356,9 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid } final JsonNode schemaNode; if (isYaml(schemaUri)) { - schemaNode = yamlMapper.readTree(inputStream); + schemaNode = getYamlMapper().readTree(inputStream); } else { - schemaNode = jsonMapper.readTree(inputStream); + schemaNode = getJsonMapper().readTree(inputStream); } final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode, config); @@ -385,22 +395,68 @@ public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode) { return newJsonSchema(SchemaLocation.of(schemaUri.toString()), jsonNode, null); } + /** + * Gets the schema. + * + * @param schemaUri the absolute IRI of the schema which can map to the retrieval IRI. + * @return the schema + */ public JsonSchema getSchema(final SchemaLocation schemaUri) { return getSchema(schemaUri, new SchemaValidatorsConfig()); } + /** + * Gets the schema. + * + * @param schemaUri the base absolute IRI + * @param jsonNode the node + * @param config the config + * @return the schema + */ public JsonSchema getSchema(final SchemaLocation schemaUri, final JsonNode jsonNode, final SchemaValidatorsConfig config) { return newJsonSchema(schemaUri, jsonNode, config); } + /** + * Gets the schema. + * + * @param schemaUri the base absolute IRI + * @param jsonNode the node + * @return the schema + */ public JsonSchema getSchema(final SchemaLocation schemaUri, final JsonNode jsonNode) { return newJsonSchema(schemaUri, jsonNode, null); } + /** + * Gets the schema. + *

+ * Using this is not recommended as there is potentially no base IRI for + * resolving references to the absolute IRI. + *

+ * Prefer {@link #getSchema(SchemaLocation, JsonNode, SchemaValidatorsConfig)} + * instead to ensure the base IRI if no id is present. + * + * @param jsonNode the node + * @param config the config + * @return the schema + */ public JsonSchema getSchema(final JsonNode jsonNode, final SchemaValidatorsConfig config) { return newJsonSchema(null, jsonNode, config); } + /** + * Gets the schema. + *

+ * Using this is not recommended as there is potentially no base IRI for + * resolving references to the absolute IRI. + *

+ * Prefer {@link #getSchema(SchemaLocation, JsonNode)} instead to ensure the + * base IRI if no id is present. + * + * @param jsonNode the node + * @return the schema + */ public JsonSchema getSchema(final JsonNode jsonNode) { return newJsonSchema(null, jsonNode, null); } diff --git a/src/main/java/com/networknt/schema/YamlMapperFactory.java b/src/main/java/com/networknt/schema/YamlMapperFactory.java new file mode 100644 index 000000000..9a9741e98 --- /dev/null +++ b/src/main/java/com/networknt/schema/YamlMapperFactory.java @@ -0,0 +1,36 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; + +/** + * YAML Mapper Factory. + */ +public class YamlMapperFactory { + + /** + * The holder defers the classloading until it is used. + */ + private static class Holder { + private static final ObjectMapper INSTANCE = YAMLMapper.builder().build(); + } + + public static ObjectMapper getInstance() { + return Holder.INSTANCE; + } +} diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java index fc718acf9..71a8a7556 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java @@ -52,7 +52,7 @@ private JsonSchema getJsonSchemaFromDataNode(JsonNode dataNode) { private JsonNode getJsonNodeFromPath(String dataPath) { InputStream dataInputStream = getClass().getResourceAsStream(dataPath); - ObjectMapper mapper = new ObjectMapper(); + ObjectMapper mapper = JsonMapperFactory.getInstance(); try { return mapper.readTree(dataInputStream); } catch(IOException e) { diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java index f318f164f..b2adaba44 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java @@ -46,7 +46,7 @@ public abstract class AbstractJsonSchemaTestSuite extends HTTPServiceSupport { - protected ObjectMapper mapper = new ObjectMapper(); + protected ObjectMapper mapper = JsonMapperFactory.getInstance(); private static String toForwardSlashPath(Path file) { return file.toString().replace('\\', '/'); @@ -185,7 +185,7 @@ private JsonSchemaFactory buildValidatorFactory(VersionFlag defaultVersion, Test JsonSchemaFactory base = JsonSchemaFactory.getInstance(specVersion); return JsonSchemaFactory .builder(base) - .objectMapper(this.mapper) + .jsonMapper(this.mapper) .schemaMappers(schemaMappers -> schemaMappers .mapPrefix("https://", "http://") .mapPrefix("http://json-schema.org", "resource:")) diff --git a/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java b/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java index ad74d5e32..d5444b260 100644 --- a/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java +++ b/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java @@ -29,7 +29,7 @@ */ public class BaseJsonSchemaValidatorTest { - private static final ObjectMapper mapper = new ObjectMapper(); + private static final ObjectMapper mapper = JsonMapperFactory.getInstance(); public static JsonNode getJsonNodeFromClasspath(String name) throws IOException { InputStream is1 = Thread.currentThread().getContextClassLoader() diff --git a/src/test/java/com/networknt/schema/Issue285Test.java b/src/test/java/com/networknt/schema/Issue285Test.java index ff868ccae..ce77eeeee 100644 --- a/src/test/java/com/networknt/schema/Issue285Test.java +++ b/src/test/java/com/networknt/schema/Issue285Test.java @@ -15,7 +15,7 @@ public class Issue285Test { private ObjectMapper mapper = new ObjectMapper(); private JsonSchemaFactory schemaFactory = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)) - .objectMapper(mapper) + .jsonMapper(mapper) .schemaMappers(schemaMappers -> schemaMappers .mapPrefix("http://json-schema.org", "resource:") .mapPrefix("https://json-schema.org", "resource:")) diff --git a/src/test/java/com/networknt/schema/Issue366FailFastTest.java b/src/test/java/com/networknt/schema/Issue366FailFastTest.java index 6267f51f1..eab951fc6 100644 --- a/src/test/java/com/networknt/schema/Issue366FailFastTest.java +++ b/src/test/java/com/networknt/schema/Issue366FailFastTest.java @@ -27,7 +27,7 @@ private void setupSchema() throws IOException { schemaValidatorsConfig.setFailFast(true); JsonSchemaFactory schemaFactory = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)) - .objectMapper(objectMapper) + .jsonMapper(objectMapper) .build(); schemaValidatorsConfig.setTypeLoose(false); diff --git a/src/test/java/com/networknt/schema/Issue366FailSlowTest.java b/src/test/java/com/networknt/schema/Issue366FailSlowTest.java index 4b6814909..7802d4f38 100644 --- a/src/test/java/com/networknt/schema/Issue366FailSlowTest.java +++ b/src/test/java/com/networknt/schema/Issue366FailSlowTest.java @@ -26,7 +26,7 @@ private void setupSchema() throws IOException { SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); JsonSchemaFactory schemaFactory = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)) - .objectMapper(objectMapper) + .jsonMapper(objectMapper) .build(); schemaValidatorsConfig.setTypeLoose(false); diff --git a/src/test/java/com/networknt/schema/Issue428Test.java b/src/test/java/com/networknt/schema/Issue428Test.java index 5c9114329..c2d33188e 100644 --- a/src/test/java/com/networknt/schema/Issue428Test.java +++ b/src/test/java/com/networknt/schema/Issue428Test.java @@ -15,7 +15,7 @@ public class Issue428Test extends HTTPServiceSupport { protected ObjectMapper mapper = new ObjectMapper(); protected JsonSchemaFactory validatorFactory = JsonSchemaFactory - .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)).objectMapper(mapper).build(); + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)).jsonMapper(mapper).build(); private void runTestFile(String testCaseFile) throws Exception { final SchemaLocation testCaseFileUri = SchemaLocation.of("classpath:" + testCaseFile); diff --git a/src/test/java/com/networknt/schema/Issue510Test.java b/src/test/java/com/networknt/schema/Issue510Test.java index 6b251e34d..2e2da1c17 100644 --- a/src/test/java/com/networknt/schema/Issue510Test.java +++ b/src/test/java/com/networknt/schema/Issue510Test.java @@ -7,7 +7,7 @@ public class Issue510Test { @Test public void testIssue510() { ObjectMapper objectMapper = new ObjectMapper(); - JsonSchemaFactory schemaFactory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).objectMapper(objectMapper).build(); + JsonSchemaFactory schemaFactory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).jsonMapper(objectMapper).build(); System.out.println("schemaFactory = " + schemaFactory); } } diff --git a/src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java b/src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java index 3c7fae236..157efc336 100644 --- a/src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java @@ -15,7 +15,7 @@ public class OpenAPI30JsonSchemaTest extends HTTPServiceSupport { protected ObjectMapper mapper = new ObjectMapper(); protected JsonSchemaFactory validatorFactory = JsonSchemaFactory - .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)).objectMapper(mapper).build(); + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)).jsonMapper(mapper).build(); public OpenAPI30JsonSchemaTest() { } diff --git a/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java b/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java index 7e6a6d49d..4b7e74265 100644 --- a/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java +++ b/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java @@ -21,7 +21,7 @@ public void testSchema1() throws IOException { ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readTree(this.json); - JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)).objectMapper(mapper).build(); + JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)).jsonMapper(mapper).build(); JsonSchema jsonSchema = factory.getSchema(schema1); Set errors = jsonSchema.validate(jsonNode); @@ -35,7 +35,7 @@ public void testSchema2() throws IOException { ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readTree(this.json); - JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)).objectMapper(mapper).build(); + JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)).jsonMapper(mapper).build(); JsonSchema jsonSchema = factory.getSchema(schema2); Set errors = jsonSchema.validate(jsonNode); @@ -48,7 +48,7 @@ public void testSchema3() throws IOException { ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readTree(this.json); - JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)).objectMapper(mapper).build(); + JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)).jsonMapper(mapper).build(); JsonSchema jsonSchema = factory.getSchema(schema3); Set errors = jsonSchema.validate(jsonNode); diff --git a/src/test/java/com/networknt/schema/V4JsonSchemaTest.java b/src/test/java/com/networknt/schema/V4JsonSchemaTest.java index d0c15a17c..59397702a 100644 --- a/src/test/java/com/networknt/schema/V4JsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/V4JsonSchemaTest.java @@ -105,7 +105,7 @@ private Set validateFailingFastSchemaFor(final String schemaF config.setFailFast(true); return JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)) - .objectMapper(objectMapper) + .jsonMapper(objectMapper) .build() .getSchema(schema, config) .validate(dataFile); diff --git a/src/test/java/com/networknt/schema/suite/TestSource.java b/src/test/java/com/networknt/schema/suite/TestSource.java index 7d607e230..1d606d884 100644 --- a/src/test/java/com/networknt/schema/suite/TestSource.java +++ b/src/test/java/com/networknt/schema/suite/TestSource.java @@ -12,10 +12,11 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.networknt.schema.JsonMapperFactory; public class TestSource { protected static final TypeReference> testCaseType = new TypeReference>() { /* intentionally empty */}; - private static final ObjectMapper mapper = new ObjectMapper(); + private static final ObjectMapper mapper = JsonMapperFactory.getInstance(); /** * Indicates whether this test-source should be executed From 9f5184eaeb4a05ea06220312e0fa489cb26fc85e Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:39:35 +0800 Subject: [PATCH 28/65] Support vocabularies --- .../com/networknt/schema/JsonMetaSchema.java | 12 +++ .../networknt/schema/JsonSchemaFactory.java | 42 ++++++---- .../com/networknt/schema/Vocabularies.java | 83 +++++++++++++++++++ .../schema/JsonSchemaTestSuiteTest.java | 5 +- 4 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/networknt/schema/Vocabularies.java diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index c31706512..de765030c 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -163,6 +163,18 @@ public Builder idKeyword(String idKeyword) { public JsonMetaSchema build() { // create builtin keywords with (custom) formats. Map kwords = createKeywordsMap(this.keywords, this.formats); + if (this.specification != null) { + if (this.specification.getVersionFlagValue() >= SpecVersion.VersionFlag.V201909.getVersionFlagValue()) { + if (!this.uri.equals(this.specification.getId())) { + String validation = Vocabularies.getVocabulary(specification, "validation"); + if (!this.vocabularies.getOrDefault(validation, false)) { + for (String keywordToRemove : Vocabularies.getKeywords("validation")) { + kwords.remove(keywordToRemove); + } + } + } + } + } return new JsonMetaSchema(this.uri, this.idKeyword, kwords, this.vocabularies, this.specification); } } diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index fb5bb41b5..2a5b6445e 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -28,6 +28,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; +import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; @@ -255,7 +256,7 @@ protected ValidationContext createValidationContext(final JsonNode schemaNode, S private JsonMetaSchema getMetaSchema(final JsonNode schemaNode, SchemaValidatorsConfig config) { final JsonNode uriNode = schemaNode.get("$schema"); if (uriNode != null && uriNode.isTextual()) { - return jsonMetaSchemas.computeIfAbsent(normalizeMetaSchemaUri(uriNode.textValue()), id -> fromId(id, config)); + return jsonMetaSchemas.computeIfAbsent(normalizeMetaSchemaUri(uriNode.textValue()), id -> getMetaSchema(id, config)); } return null; } @@ -266,34 +267,39 @@ private JsonMetaSchema findMetaSchemaForSchema(final JsonNode schemaNode, Schema throw new JsonSchemaException("Unknown MetaSchema: " + uriNode.toString()); } final String uri = uriNode == null || uriNode.isNull() ? defaultMetaSchemaURI : normalizeMetaSchemaUri(uriNode.textValue()); - final JsonMetaSchema jsonMetaSchema = jsonMetaSchemas.computeIfAbsent(uri, id -> fromId(id, config)); + final JsonMetaSchema jsonMetaSchema = jsonMetaSchemas.computeIfAbsent(uri, id -> getMetaSchema(id, config)); return jsonMetaSchema; } - private JsonMetaSchema fromId(String id, SchemaValidatorsConfig config) { + public JsonMetaSchema getMetaSchema(String id, SchemaValidatorsConfig config) { // Is it a well-known dialect? return SpecVersionDetector.detectOptionalVersion(id) .map(JsonSchemaFactory::checkVersion) .map(JsonSchemaVersion::getInstance) .orElseGet(() -> { // Custom meta schema - JsonSchema schema = getSchema(SchemaLocation.of(id), config); - JsonMetaSchema.Builder builder = JsonMetaSchema.builder(id, schema.getValidationContext().getMetaSchema()); - VersionFlag specification = schema.getValidationContext().getMetaSchema().getSpecification(); - if (specification != null) { - if (specification.getVersionFlagValue() >= VersionFlag.V201909.getVersionFlagValue()) { - // Process vocabularies - JsonNode vocabulary = schema.getSchemaNode().get("$vocabulary"); - if (vocabulary != null) { - for(Entry vocabs : vocabulary.properties()) { - builder.vocabulary(vocabs.getKey(), vocabs.getValue().booleanValue()); - } - } - + return loadMetaSchema(id, config); + }); + } + + protected JsonMetaSchema loadMetaSchema(String id, SchemaValidatorsConfig config) { + JsonSchema schema = getSchema(SchemaLocation.of(id), config); + JsonMetaSchema.Builder builder = JsonMetaSchema.builder(id, schema.getValidationContext().getMetaSchema()); + VersionFlag specification = schema.getValidationContext().getMetaSchema().getSpecification(); + if (specification != null) { + if (specification.getVersionFlagValue() >= VersionFlag.V201909.getVersionFlagValue()) { + // Process vocabularies + JsonNode vocabulary = schema.getSchemaNode().get("$vocabulary"); + if (vocabulary != null) { + builder.vocabularies(new HashMap<>()); + for(Entry vocabs : vocabulary.properties()) { + builder.vocabulary(vocabs.getKey(), vocabs.getValue().booleanValue()); } } - return builder.build(); - }); + + } + } + return builder.build(); } public JsonSchema getSchema(final String schema, final SchemaValidatorsConfig config) { diff --git a/src/main/java/com/networknt/schema/Vocabularies.java b/src/main/java/com/networknt/schema/Vocabularies.java new file mode 100644 index 000000000..c43e10d99 --- /dev/null +++ b/src/main/java/com/networknt/schema/Vocabularies.java @@ -0,0 +1,83 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Vocabularies. + */ +public class Vocabularies { + private static final Map> KEYWORDS_MAPPING; + + static { + Map> mapping = new HashMap<>(); + List validation = new ArrayList<>(); + validation.add("type"); + validation.add("enum"); + validation.add("const"); + + validation.add("multipleOf"); + validation.add("maximum"); + validation.add("exclusiveMaximum"); + validation.add("minimum"); + validation.add("exclusiveMinimum"); + + validation.add("maxLength"); + validation.add("minLength"); + validation.add("pattern"); + + validation.add("maxItems"); + validation.add("minItems"); + validation.add("uniqueItems"); + validation.add("maxContains"); + validation.add("minContains"); + + validation.add("maxProperties"); + validation.add("minProperties"); + validation.add("required"); + validation.add("dependentRequired"); + + mapping.put("validation", validation); + + KEYWORDS_MAPPING = mapping; + } + + /** + * Gets the keywords associated with a vocabulary. + * + * @param vocabulary the vocabulary + * @return the keywords + */ + public static List getKeywords(String vocabulary) { + return KEYWORDS_MAPPING.get(vocabulary); + } + + /** + * Gets the vocabulary IRI. + * + * @param specification the specification + * @param vocabulary the vocabulary + * @return the vocabulary IRI + */ + public static String getVocabulary(SpecVersion.VersionFlag specification, String vocabulary) { + String base = specification.getId().substring(0, specification.getId().lastIndexOf('/')); + return base + "/vocab/" + vocabulary; + } +} diff --git a/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java b/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java index a1a398711..eab976d5a 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java @@ -69,12 +69,11 @@ protected Optional reason(Path path) { } private void disableV202012Tests() { - //this.disabled.put(Paths.get("src/test/suite/tests/draft2020-12/optional/format-assertion.json"), "Unsupported behavior"); - this.disabled.put(Paths.get("src/test/suite/tests/draft2020-12/vocabulary.json"), "Unsupported behavior"); + // nothing here } private void disableV201909Tests() { - this.disabled.put(Paths.get("src/test/suite/tests/draft2019-09/vocabulary.json"), "Unsupported behavior"); + // nothing here } private void disableV7Tests() { From 9c1efab2e8e3d8eb58db9240724afa5dace46fde Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:53:26 +0800 Subject: [PATCH 29/65] Refactor --- .../networknt/schema/uri/SchemaLoader.java | 10 +++- .../networknt/schema/uri/SchemaLoaders.java | 49 +++++++++++++++---- .../networknt/schema/uri/SchemaMapper.java | 9 +++- .../networknt/schema/uri/SchemaMappers.java | 49 ++++++++++++++++--- .../com/networknt/schema/Issue665Test.java | 2 +- 5 files changed, 99 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/networknt/schema/uri/SchemaLoader.java b/src/main/java/com/networknt/schema/uri/SchemaLoader.java index 8f1cc7fa5..5e142cdba 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaLoader.java +++ b/src/main/java/com/networknt/schema/uri/SchemaLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 the original author or authors. + * 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. @@ -18,9 +18,15 @@ import com.networknt.schema.AbsoluteIri; /** - * Loader for schema. + * Schema Loader used to load a schema given the retrieval IRI. */ @FunctionalInterface public interface SchemaLoader { + /** + * Loads a schema given the retrieval IRI. + * + * @param absoluteIri the retrieval IRI + * @return the input stream source + */ InputStreamSource getSchema(AbsoluteIri absoluteIri); } diff --git a/src/main/java/com/networknt/schema/uri/SchemaLoaders.java b/src/main/java/com/networknt/schema/uri/SchemaLoaders.java index a433daac9..f1b26da88 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaLoaders.java +++ b/src/main/java/com/networknt/schema/uri/SchemaLoaders.java @@ -23,7 +23,7 @@ import java.util.function.Function; /** - * Schema Loaders. + * Schema Loaders used to load a schema given the retrieval IRI. */ public class SchemaLoaders extends ArrayList { private static final long serialVersionUID = 1L; @@ -56,7 +56,7 @@ public static Builder builder() { } public static class Builder { - SchemaLoaders values = new SchemaLoaders(); + private SchemaLoaders values = new SchemaLoaders(); public Builder() { } @@ -72,26 +72,55 @@ public Builder with(Builder builder) { return this; } - public Builder values(Consumer> values) { - values.accept(this.values); + /** + * Customize the schema loaders. + * + * @param customizer the customizer + * @return the builder + */ + public Builder values(Consumer> customizer) { + customizer.accept(this.values); return this; } - + + /** + * Adds a schema loader. + * + * @param schemaLoader the schema loader + * @return the builder + */ public Builder add(SchemaLoader schemaLoader) { this.values.add(schemaLoader); return this; } - public Builder values(Map mappings) { - this.values.add(new MapSchemaLoader(mappings)); + /** + * Sets the schema data by absolute IRI. + * + * @param schemas the map of IRI to schema data + * @return the builder + */ + public Builder schemas(Map schemas) { + this.values.add(new MapSchemaLoader(schemas)); return this; } - - public Builder values(Function mappings) { - this.values.add(new MapSchemaLoader(mappings)); + + /** + * Sets the schema data by absolute IRI function. + * + * @param schemas the function that returns schema data given IRI + * @return the builder + */ + public Builder schemas(Function schemas) { + this.values.add(new MapSchemaLoader(schemas)); return this; } + /** + * Builds a {@link SchemaLoaders}. + * + * @return the schema loaders + */ public SchemaLoaders build() { if (this.values.isEmpty()) { return DEFAULT; diff --git a/src/main/java/com/networknt/schema/uri/SchemaMapper.java b/src/main/java/com/networknt/schema/uri/SchemaMapper.java index 914a61018..8cf3bca81 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaMapper.java +++ b/src/main/java/com/networknt/schema/uri/SchemaMapper.java @@ -18,9 +18,16 @@ import com.networknt.schema.AbsoluteIri; /** - * Maps absolute IRI. + * Schema Mapper used to map an ID indicated by an absolute IRI to a retrieval + * IRI. */ @FunctionalInterface public interface SchemaMapper { + /** + * Maps an ID indicated by an absolute IRI to a retrieval IRI. + * + * @param absoluteIRI the ID + * @return the retrieval IRI or null if this mapper doesn't support the mapping + */ AbsoluteIri map(AbsoluteIri absoluteIRI); } diff --git a/src/main/java/com/networknt/schema/uri/SchemaMappers.java b/src/main/java/com/networknt/schema/uri/SchemaMappers.java index 1a21abdab..317eb1199 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaMappers.java +++ b/src/main/java/com/networknt/schema/uri/SchemaMappers.java @@ -23,7 +23,8 @@ import java.util.function.Function; /** - * Schema Mappers. + * Schema Mappers used to map an ID indicated by an absolute IRI to a retrieval + * IRI. */ public class SchemaMappers extends ArrayList { private static final long serialVersionUID = 1L; @@ -61,31 +62,67 @@ public Builder with(Builder builder) { return this; } - public Builder values(Consumer> values) { - values.accept(this.values); + /** + * Customize the schema mappers. + * + * @param customizer the customizer + * @return the builder + */ + public Builder values(Consumer> customizer) { + customizer.accept(this.values); return this; } + /** + * Adds a schema mapper. + * + * @param schemaMapper the schema mapper + * @return the builder + */ public Builder add(SchemaMapper schemaMapper) { this.values.add(schemaMapper); return this; } + /** + * Maps a schema given a source prefix with a replacement. + * + * @param source the source prefix + * @param replacement the replacement prefix + * @return the builder + */ public Builder mapPrefix(String source, String replacement) { this.values.add(new PrefixSchemaMapper(source, replacement)); return this; } - public Builder values(Map mappings) { + /** + * Sets the mappings. + * + * @param mappings the mappings + * @return the builder + */ + public Builder mappings(Map mappings) { this.values.add(new MapSchemaMapper(mappings)); return this; } - - public Builder values(Function mappings) { + + /** + * Sets the function that maps the IRI to another IRI. + * + * @param mappings the mappings + * @return the builder + */ + public Builder mappings(Function mappings) { this.values.add(new MapSchemaMapper(mappings)); return this; } + /** + * Builds a {@link SchemaMappers} + * + * @return the schema mappers + */ public SchemaMappers build() { return values; } diff --git a/src/test/java/com/networknt/schema/Issue665Test.java b/src/test/java/com/networknt/schema/Issue665Test.java index 2973cc4f1..23044c0a2 100644 --- a/src/test/java/com/networknt/schema/Issue665Test.java +++ b/src/test/java/com/networknt/schema/Issue665Test.java @@ -26,7 +26,7 @@ void testUrnUriAsLocalRef_ExternalURN() { JsonSchemaFactory factory = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)) .schemaMappers(schemaMappers -> { - schemaMappers.values(Collections.singletonMap("urn:data", + schemaMappers.mappings(Collections.singletonMap("urn:data", "classpath:draft7/urn/issue665_external_urn_subschema.json")); }) .build(); From e20e302120007389f4e431c6400b53ea7ea3a268 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:17:11 +0800 Subject: [PATCH 30/65] Refactor --- .../networknt/schema/JsonSchemaFactory.java | 98 ++++++++++++++++--- 1 file changed, 84 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 2a5b6445e..dd4533471 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -152,9 +152,7 @@ public SchemaLoader getSchemaLoader() { /** * Builder without keywords or formats. * - * - * JsonSchemaFactory.builder(JsonSchemaFactory.getDraftV4()).build(); - * + * Typically {@link #builder(JsonSchemaFactory)} is what is required. * * @return a builder instance without any keywords or formats - usually not what one needs. */ @@ -195,6 +193,16 @@ public static JsonSchemaVersion checkVersion(SpecVersion.VersionFlag versionFlag } } + /** + * Builder from an existing {@link JsonSchemaFactory}. + *

+ * + * JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)); + * + * + * @param blueprint the existing factory + * @return the builder + */ public static Builder builder(final JsonSchemaFactory blueprint) { Builder builder = builder() .addMetaSchemas(blueprint.jsonMetaSchemas.values()) @@ -249,7 +257,7 @@ protected SchemaLocation getSchemaLocation(SchemaLocation schemaRetrievalUri, Js } protected ValidationContext createValidationContext(final JsonNode schemaNode, SchemaValidatorsConfig config) { - final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode, config); + final JsonMetaSchema jsonMetaSchema = getMetaSchemaOrDefault(schemaNode, config); return new ValidationContext(jsonMetaSchema, this, config); } @@ -261,14 +269,13 @@ private JsonMetaSchema getMetaSchema(final JsonNode schemaNode, SchemaValidators return null; } - private JsonMetaSchema findMetaSchemaForSchema(final JsonNode schemaNode, SchemaValidatorsConfig config) { + private JsonMetaSchema getMetaSchemaOrDefault(final JsonNode schemaNode, SchemaValidatorsConfig config) { final JsonNode uriNode = schemaNode.get("$schema"); if (uriNode != null && !uriNode.isNull() && !uriNode.isTextual()) { throw new JsonSchemaException("Unknown MetaSchema: " + uriNode.toString()); } final String uri = uriNode == null || uriNode.isNull() ? defaultMetaSchemaURI : normalizeMetaSchemaUri(uriNode.textValue()); - final JsonMetaSchema jsonMetaSchema = jsonMetaSchemas.computeIfAbsent(uri, id -> getMetaSchema(id, config)); - return jsonMetaSchema; + return jsonMetaSchemas.computeIfAbsent(uri, id -> getMetaSchema(id, config)); } public JsonMetaSchema getMetaSchema(String id, SchemaValidatorsConfig config) { @@ -302,6 +309,16 @@ protected JsonMetaSchema loadMetaSchema(String id, SchemaValidatorsConfig config return builder.build(); } + /** + * Gets the schema. + *

+ * Using this is not recommended as there is potentially no base IRI for + * resolving references to the absolute IRI. + * + * @param schema the schema data as a string + * @param config the config + * @return the schema + */ public JsonSchema getSchema(final String schema, final SchemaValidatorsConfig config) { try { final JsonNode schemaNode = getJsonMapper().readTree(schema); @@ -312,10 +329,29 @@ public JsonSchema getSchema(final String schema, final SchemaValidatorsConfig co } } + /** + * Gets the schema. + *

+ * Using this is not recommended as there is potentially no base IRI for + * resolving references to the absolute IRI. + * + * @param schema the schema data as a string + * @return the schema + */ public JsonSchema getSchema(final String schema) { - return getSchema(schema, null); + return getSchema(schema, createSchemaValidatorsConfig()); } + /** + * Gets the schema. + *

+ * Using this is not recommended as there is potentially no base IRI for + * resolving references to the absolute IRI. + * + * @param schemaStream the input stream with the schema data + * @param config the config + * @return the schema + */ public JsonSchema getSchema(final InputStream schemaStream, final SchemaValidatorsConfig config) { try { final JsonNode schemaNode = getJsonMapper().readTree(schemaStream); @@ -326,8 +362,17 @@ public JsonSchema getSchema(final InputStream schemaStream, final SchemaValidato } } + /** + * Gets the schema. + *

+ * Using this is not recommended as there is potentially no base IRI for + * resolving references to the absolute IRI. + * + * @param schemaStream the input stream with the schema data + * @return the schema + */ public JsonSchema getSchema(final InputStream schemaStream) { - return getSchema(schemaStream, null); + return getSchema(schemaStream, createSchemaValidatorsConfig()); } /** @@ -354,7 +399,11 @@ protected ObjectMapper getYamlMapper() { protected ObjectMapper getJsonMapper() { return this.jsonMapper != null ? this.jsonMapper : JsonMapperFactory.getInstance(); } - + + protected SchemaValidatorsConfig createSchemaValidatorsConfig() { + return new SchemaValidatorsConfig(); + } + protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValidatorsConfig config) { try (InputStream inputStream = this.schemaLoader.getSchema(schemaUri.getAbsoluteIri()).getInputStream()) { if (inputStream == null) { @@ -367,7 +416,7 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid schemaNode = getJsonMapper().readTree(inputStream); } - final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode, config); + final JsonMetaSchema jsonMetaSchema = getMetaSchemaOrDefault(schemaNode, config); JsonNodePath evaluationPath = new JsonNodePath(config.getPathType()); JsonSchema jsonSchema; SchemaLocation schemaLocation = SchemaLocation.of(schemaUri.toString()); @@ -389,16 +438,37 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid } } + /** + * Gets the schema. + * + * @param schemaUri the absolute IRI of the schema which can map to the retrieval IRI. + * @return the schema + */ public JsonSchema getSchema(final URI schemaUri) { - return getSchema(SchemaLocation.of(schemaUri.toString()), new SchemaValidatorsConfig()); + return getSchema(SchemaLocation.of(schemaUri.toString()), createSchemaValidatorsConfig()); } + /** + * Gets the schema. + * + * @param schemaUri the absolute IRI of the schema which can map to the retrieval IRI. + * @param jsonNode the node + * @param config the config + * @return the schema + */ public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode, final SchemaValidatorsConfig config) { return newJsonSchema(SchemaLocation.of(schemaUri.toString()), jsonNode, config); } + /** + * Gets the schema. + * + * @param schemaUri the absolute IRI of the schema which can map to the retrieval IRI. + * @param jsonNode the node + * @return the schema + */ public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode) { - return newJsonSchema(SchemaLocation.of(schemaUri.toString()), jsonNode, null); + return newJsonSchema(SchemaLocation.of(schemaUri.toString()), jsonNode, createSchemaValidatorsConfig()); } /** @@ -408,7 +478,7 @@ public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode) { * @return the schema */ public JsonSchema getSchema(final SchemaLocation schemaUri) { - return getSchema(schemaUri, new SchemaValidatorsConfig()); + return getSchema(schemaUri, createSchemaValidatorsConfig()); } /** From 83510a30ce219acc7d635138a3488fe7fe4fe38b Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:30:35 +0800 Subject: [PATCH 31/65] Refactor --- .../java/com/networknt/schema/JsonSchema.java | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index a2599daa2..a2a193771 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -502,10 +502,16 @@ public Set validate(ExecutionContext executionContext, JsonNo /** * Validate the given root JsonNode, starting at the root of the data path. + *

+ * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. + * * @param rootNode the root node - * - * @return A list of ValidationMessage if there is any validation error, or an empty - * list if there is no error. + * @return A list of ValidationMessage if there is any validation error, or an + * empty list if there is no error. */ public Set validate(JsonNode rootNode) { return validate(rootNode, OutputFormat.DEFAULT); @@ -513,9 +519,16 @@ public Set validate(JsonNode rootNode) { /** * Validate the given root JsonNode, starting at the root of the data path. - * @param rootNode the root node + *

+ * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. + * + * @param rootNode the root node * @param executionCustomizer the execution customizer - * @return + * @return the assertions */ public Set validate(JsonNode rootNode, ExecutionCustomizer executionCustomizer) { return validate(rootNode, OutputFormat.DEFAULT, executionCustomizer); @@ -523,9 +536,16 @@ public Set validate(JsonNode rootNode, ExecutionCustomizer ex /** * Validate the given root JsonNode, starting at the root of the data path. - * @param rootNode the root node + *

+ * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. + * + * @param rootNode the root node * @param executionCustomizer the execution customizer - * @return + * @return the assertions */ public Set validate(JsonNode rootNode, Consumer executionCustomizer) { return validate(rootNode, OutputFormat.DEFAULT, executionCustomizer); @@ -534,6 +554,12 @@ public Set validate(JsonNode rootNode, Consumer + * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. * * @param the result type * @param rootNode the root note @@ -547,6 +573,12 @@ public T validate(JsonNode rootNode, OutputFormat format) { /** * Validates the given root JsonNode, starting at the root of the data path. The * output will be formatted using the formatter specified. + *

+ * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. * * @param the result type * @param rootNode the root node @@ -561,6 +593,12 @@ public T validate(JsonNode rootNode, OutputFormat format, ExecutionCustom /** * Validates the given root JsonNode, starting at the root of the data path. The * output will be formatted using the formatter specified. + *

+ * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. * * @param the result type * @param rootNode the root node From e83cf8ac03143ec37ac43414f0504764b590ecd1 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:17:02 +0800 Subject: [PATCH 32/65] Remove deprecations --- .../com/networknt/schema/JsonSchemaFactory.java | 12 ------------ .../networknt/schema/SchemaValidatorsConfig.java | 16 ---------------- 2 files changed, 28 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index dd4533471..25925e96b 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -160,18 +160,6 @@ public static Builder builder() { return new Builder(); } - /** - * @deprecated - * This is a method that is kept to ensure backward compatible. You shouldn't use it anymore. - * Please specify the draft version when get an instance. - * - * @return JsonSchemaFactory - */ - @Deprecated - public static JsonSchemaFactory getInstance() { - return getInstance(SpecVersion.VersionFlag.V4); - } - public static JsonSchemaFactory getInstance(SpecVersion.VersionFlag versionFlag) { JsonSchemaVersion jsonSchemaVersion = checkVersion(versionFlag); JsonMetaSchema metaSchema = jsonSchemaVersion.getInstance(); diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index d2790f96b..ee1bb9ed7 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -421,22 +421,6 @@ public boolean isWriteMode() { return null == this.writeOnly || this.writeOnly; } - /** - * - * When set to true considers that schema is used to write data then ReadOnlyValidator is activated. Default true. - * - * @param writeMode true if schema is used to write data - * @deprecated Use {@code setReadOnly} or {@code setWriteOnly} - */ - @Deprecated - public void setWriteMode(boolean writeMode) { - if (writeMode) { - setWriteOnly(true); - } else { - setReadOnly(true); - } - } - /** * Set the approach used to generate paths in messages, logs and errors (default is PathType.LEGACY). * From 0b6009cd60eff1523f8317d5959ba06fba6d1dd7 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:18:04 +0800 Subject: [PATCH 33/65] Update uri schema loader --- .../networknt/schema/uri/UriSchemaLoader.java | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java b/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java index 863e7bc4c..f1842e02c 100644 --- a/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java +++ b/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java @@ -15,7 +15,12 @@ */ package com.networknt.schema.uri; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; import java.net.URI; +import java.net.URL; +import java.net.URLConnection; import com.networknt.schema.AbsoluteIri; @@ -26,6 +31,48 @@ public class UriSchemaLoader implements SchemaLoader { @Override public InputStreamSource getSchema(AbsoluteIri absoluteIri) { URI uri = URI.create(absoluteIri.toString()); - return () -> uri.toURL().openStream(); + return () -> { + URLConnection conn = uri.toURL().openConnection(); + return this.openConnectionCheckRedirects(conn); + }; + } + + // https://www.cs.mun.ca/java-api-1.5/guide/deployment/deployment-guide/upgrade-guide/article-17.html + protected InputStream openConnectionCheckRedirects(URLConnection c) throws IOException { + boolean redir; + int redirects = 0; + InputStream in = null; + do { + if (c instanceof HttpURLConnection) { + ((HttpURLConnection) c).setInstanceFollowRedirects(false); + } + // We want to open the input stream before getting headers + // because getHeaderField() et al swallow IOExceptions. + in = c.getInputStream(); + redir = false; + if (c instanceof HttpURLConnection) { + HttpURLConnection http = (HttpURLConnection) c; + int stat = http.getResponseCode(); + if (stat >= 300 && stat <= 307 && stat != 306 && stat != HttpURLConnection.HTTP_NOT_MODIFIED) { + URL base = http.getURL(); + String loc = http.getHeaderField("Location"); + URL target = null; + if (loc != null) { + target = new URL(base, loc); + } + http.disconnect(); + // Redirection should be allowed only for HTTP and HTTPS + // and should be limited to 5 redirections at most. + if (target == null || !(target.getProtocol().equals("http") || target.getProtocol().equals("https")) + || redirects >= 5) { + throw new SecurityException("illegal URL redirect"); + } + redir = true; + c = target.openConnection(); + redirects++; + } + } + } while (redir); + return in; } } From a2bb3496d052d5f6d783cac41dfff13186181b87 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:42:58 +0800 Subject: [PATCH 34/65] Refactor --- .../com/networknt/schema/Version201909.java | 21 ++++++++++++----- .../com/networknt/schema/Version202012.java | 23 +++++++++++++------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/networknt/schema/Version201909.java b/src/main/java/com/networknt/schema/Version201909.java index 75b22ef06..f8522f853 100644 --- a/src/main/java/com/networknt/schema/Version201909.java +++ b/src/main/java/com/networknt/schema/Version201909.java @@ -1,10 +1,24 @@ package com.networknt.schema; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; public class Version201909 extends JsonSchemaVersion{ private static final String URI = "https://json-schema.org/draft/2019-09/schema"; private static final String ID = "$id"; + private static final Map VOCABULARY; + + static { + Map vocabulary = new HashMap<>(); + vocabulary.put("https://json-schema.org/draft/2019-09/vocab/core", true); + vocabulary.put("https://json-schema.org/draft/2019-09/vocab/applicator", true); + vocabulary.put("https://json-schema.org/draft/2019-09/vocab/validation", true); + vocabulary.put("https://json-schema.org/draft/2019-09/vocab/meta-data", true); + vocabulary.put("https://json-schema.org/draft/2019-09/vocab/format", false); + vocabulary.put("https://json-schema.org/draft/2019-09/vocab/content", true); + VOCABULARY = vocabulary; + } static { // add version specific formats here. @@ -38,12 +52,7 @@ public JsonMetaSchema getInstance() { new NonValidationKeyword("then"), new NonValidationKeyword("else") )) - .vocabulary("https://json-schema.org/draft/2019-09/vocab/core") - .vocabulary("https://json-schema.org/draft/2019-09/vocab/applicator") - .vocabulary("https://json-schema.org/draft/2019-09/vocab/validation") - .vocabulary("https://json-schema.org/draft/2019-09/vocab/meta-data") - .vocabulary("https://json-schema.org/draft/2019-09/vocab/format", false) - .vocabulary("https://json-schema.org/draft/2019-09/vocab/content") + .vocabularies(VOCABULARY) .build(); } } diff --git a/src/main/java/com/networknt/schema/Version202012.java b/src/main/java/com/networknt/schema/Version202012.java index 8cba2a48f..770a506ac 100644 --- a/src/main/java/com/networknt/schema/Version202012.java +++ b/src/main/java/com/networknt/schema/Version202012.java @@ -1,10 +1,25 @@ package com.networknt.schema; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; public class Version202012 extends JsonSchemaVersion { private static final String URI = "https://json-schema.org/draft/2020-12/schema"; private static final String ID = "$id"; + private static final Map VOCABULARY; + + static { + Map vocabulary = new HashMap<>(); + vocabulary.put("https://json-schema.org/draft/2020-12/vocab/core", true); + vocabulary.put("https://json-schema.org/draft/2020-12/vocab/applicator", true); + vocabulary.put("https://json-schema.org/draft/2020-12/vocab/unevaluated", true); + vocabulary.put("https://json-schema.org/draft/2020-12/vocab/validation", true); + vocabulary.put("https://json-schema.org/draft/2020-12/vocab/meta-data", true); + vocabulary.put("https://json-schema.org/draft/2020-12/vocab/format-annotation", true); + vocabulary.put("https://json-schema.org/draft/2020-12/vocab/content", true); + VOCABULARY = vocabulary; + } static { // add version specific formats here. @@ -38,13 +53,7 @@ public JsonMetaSchema getInstance() { new NonValidationKeyword("else"), new NonValidationKeyword("additionalItems") )) - .vocabulary("https://json-schema.org/draft/2020-12/vocab/core") - .vocabulary("https://json-schema.org/draft/2020-12/vocab/applicator") - .vocabulary("https://json-schema.org/draft/2020-12/vocab/unevaluated") - .vocabulary("https://json-schema.org/draft/2020-12/vocab/validation") - .vocabulary("https://json-schema.org/draft/2020-12/vocab/meta-data") - .vocabulary("https://json-schema.org/draft/2020-12/vocab/format-annotation") - .vocabulary("https://json-schema.org/draft/2020-12/vocab/content") + .vocabularies(VOCABULARY) .build(); } } From dc9c4ba0fd24fcb63391a76f7bb7788fec937186 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 16:01:41 +0800 Subject: [PATCH 35/65] Refactor --- .../networknt/schema/JsonSchemaFactory.java | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 25925e96b..6ccf9bc9b 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -407,17 +407,17 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid final JsonMetaSchema jsonMetaSchema = getMetaSchemaOrDefault(schemaNode, config); JsonNodePath evaluationPath = new JsonNodePath(config.getPathType()); JsonSchema jsonSchema; - SchemaLocation schemaLocation = SchemaLocation.of(schemaUri.toString()); - if (idMatchesSourceUri(jsonMetaSchema, schemaNode, schemaUri) || schemaUri.getFragment() == null + if (schemaUri.getFragment() == null || schemaUri.getFragment().getNameCount() == 0) { + // Schema without fragment ValidationContext validationContext = new ValidationContext(jsonMetaSchema, this, config); - jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, schemaNode, null, true /* retrieved via id, resolving will not change anything */); + jsonSchema = doCreate(validationContext, schemaUri, evaluationPath, schemaNode, null, true /* retrieved via id, resolving will not change anything */); } else { - // Subschema + // Schema with fragment pointing to sub schema final ValidationContext validationContext = createValidationContext(schemaNode, config); - SchemaLocation documentLocation = new SchemaLocation(schemaLocation.getAbsoluteIri()); + SchemaLocation documentLocation = new SchemaLocation(schemaUri.getAbsoluteIri()); JsonSchema document = doCreate(validationContext, documentLocation, evaluationPath, schemaNode, null, false); - return document.getRefSchema(schemaLocation.getFragment()); + return document.getRefSchema(schemaUri.getFragment()); } return jsonSchema; } catch (IOException e) { @@ -525,16 +525,6 @@ public JsonSchema getSchema(final JsonNode jsonNode) { return newJsonSchema(null, jsonNode, null); } - private boolean idMatchesSourceUri(final JsonMetaSchema metaSchema, final JsonNode schema, final SchemaLocation schemaUri) { - String id = metaSchema.readId(schema); - if (id == null || id.isEmpty()) { - return false; - } - boolean result = id.equals(schemaUri.toString()); - logger.debug("Matching {} to {}: {}", id, schemaUri, result); - return result; - } - private boolean isYaml(final SchemaLocation schemaUri) { final String schemeSpecificPart = schemaUri.getAbsoluteIri().toString(); final int idx = schemeSpecificPart.lastIndexOf('.'); From a9062d6ac7ad45638c6d1cf0323a1efdd46cbf05 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:22:59 +0800 Subject: [PATCH 36/65] Add get instance customizer --- .../java/com/networknt/schema/JsonSchemaFactory.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 6ccf9bc9b..576be0622 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -169,6 +169,16 @@ public static JsonSchemaFactory getInstance(SpecVersion.VersionFlag versionFlag) .build(); } + public static JsonSchemaFactory getInstance(SpecVersion.VersionFlag versionFlag, + Consumer customizer) { + JsonSchemaVersion jsonSchemaVersion = checkVersion(versionFlag); + JsonMetaSchema metaSchema = jsonSchemaVersion.getInstance(); + JsonSchemaFactory.Builder builder = builder().defaultMetaSchemaURI(metaSchema.getUri()) + .addMetaSchema(metaSchema); + customizer.accept(builder); + return builder.build(); + } + public static JsonSchemaVersion checkVersion(SpecVersion.VersionFlag versionFlag){ if (null == versionFlag) return null; switch (versionFlag) { From 4bb71bb5812abb656dc01a254be3fdbf01145f39 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 23 Jan 2024 21:45:26 +0800 Subject: [PATCH 37/65] Add content validation --- .../schema/ContentEncodingValidator.java | 74 ++++++++++++++ .../schema/ContentMediaTypeValidator.java | 97 +++++++++++++++++++ .../networknt/schema/ValidatorTypeCode.java | 5 +- .../com/networknt/schema/Version201909.java | 1 + .../com/networknt/schema/Version202012.java | 1 + .../java/com/networknt/schema/Version7.java | 2 - src/main/resources/jsv-messages.properties | 2 + .../schema/JsonSchemaTestSuiteTest.java | 2 +- 8 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/networknt/schema/ContentEncodingValidator.java create mode 100644 src/main/java/com/networknt/schema/ContentMediaTypeValidator.java diff --git a/src/main/java/com/networknt/schema/ContentEncodingValidator.java b/src/main/java/com/networknt/schema/ContentEncodingValidator.java new file mode 100644 index 000000000..579e67921 --- /dev/null +++ b/src/main/java/com/networknt/schema/ContentEncodingValidator.java @@ -0,0 +1,74 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.JsonNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Base64; +import java.util.Collections; +import java.util.Set; + +/** + * Validation for contentEncoding keyword. + *

+ * Note that since 2019-09 this keyword only generates annotations and not + * assertions. + */ +public class ContentEncodingValidator extends BaseJsonValidator { + private static final Logger logger = LoggerFactory.getLogger(ContentEncodingValidator.class); + private String contentEncoding; + + public ContentEncodingValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONTENT_ENCODING, + validationContext); + this.contentEncoding = schemaNode.textValue(); + } + + private boolean matches(String value) { + if ("base64".equals(this.contentEncoding)) { + try { + Base64.getDecoder().decode(value); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } else { + return true; + } + } + + @Override + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); + + // Ignore non-strings + JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); + if (nodeType != JsonType.STRING) { + return Collections.emptySet(); + } + + if (!matches(node.asText())) { + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.contentEncoding).build()); + } + return Collections.emptySet(); + } +} diff --git a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java new file mode 100644 index 000000000..36e67bd8e --- /dev/null +++ b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java @@ -0,0 +1,97 @@ +/* + * 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; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.Set; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Validation for contentMediaType keyword. + *

+ * Note that since 2019-09 this keyword only generates annotations and not assertions. + */ +public class ContentMediaTypeValidator extends BaseJsonValidator { + private static final Logger logger = LoggerFactory.getLogger(ContentMediaTypeValidator.class); + private static final String PATTERN_STRING = "(application|audio|font|example|image|message|model|multipart|text|video|x-(?:[0-9A-Za-z!#$%&'*+.^_`|~-]+))/([0-9A-Za-z!#$%&'*+.^_`|~-]+)((?:[ \t]*;[ \t]*[0-9A-Za-z!#$%&'*+.^_`|~-]+=(?:[0-9A-Za-z!#$%&'*+.^_`|~-]+|\"(?:[^\"\\\\]|\\.)*\"))*)"; + private static final Pattern PATTERN = Pattern.compile(PATTERN_STRING); + private final String contentMediaType; + + public ContentMediaTypeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONTENT_MEDIA_TYPE, validationContext); + this.contentMediaType = schemaNode.textValue(); + } + + private boolean matches(String value) { + if ("application/json".equals(this.contentMediaType)) { + // Validate content + JsonNode node = this.parentSchema.getSchemaNode().get("contentEncoding"); + String encoding = null; + if (node != null && node.isTextual()) { + encoding = node.asText(); + } + String data = value; + if ("base64".equals(encoding)) { + try { + data = new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8); + } catch(IllegalArgumentException e) { + return true; // The contentEncoding keyword will report the failure + } + } + // Validate the json + try { + JsonMapperFactory.getInstance().readTree(data); + } catch (JsonProcessingException e) { + return false; + } + return true; + } + else if (!PATTERN.matcher(this.contentMediaType).matches()) { + return false; + } else { + // validate data + } + return true; + } + + @Override + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); + + // Ignore non-strings + JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); + if (nodeType != JsonType.STRING) { + return Collections.emptySet(); + } + + if (!matches(node.asText())) { + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.contentMediaType).build()); + } + return Collections.emptySet(); + } +} diff --git a/src/main/java/com/networknt/schema/ValidatorTypeCode.java b/src/main/java/com/networknt/schema/ValidatorTypeCode.java index 98f60bd4d..44663c1f7 100644 --- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java +++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java @@ -38,7 +38,8 @@ enum VersionCode { MaxV201909(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V4, SpecVersion.VersionFlag.V6, SpecVersion.VersionFlag.V7, SpecVersion.VersionFlag.V201909 }), MinV201909(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V201909, SpecVersion.VersionFlag.V202012 }), MinV202012(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V202012 }), - V201909(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V201909 }); + V201909(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V201909 }), + V7(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V7 }); private final EnumSet versions; @@ -60,6 +61,8 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType { ANY_OF("anyOf", "1003", AnyOfValidator::new, VersionCode.AllVersions), CONST("const", "1042", ConstValidator::new, VersionCode.MinV6), CONTAINS("contains", "1043", ContainsValidator::new, VersionCode.MinV6), + CONTENT_ENCODING("contentEncoding", "1052", ContentEncodingValidator::new, VersionCode.V7), + CONTENT_MEDIA_TYPE("contentMediaType", "1053", ContentMediaTypeValidator::new, VersionCode.V7), CROSS_EDITS("crossEdits", "1004", null, VersionCode.AllVersions), DATETIME("dateTime", "1034", null, VersionCode.AllVersions), DEPENDENCIES("dependencies", "1007", DependenciesValidator::new, VersionCode.AllVersions), diff --git a/src/main/java/com/networknt/schema/Version201909.java b/src/main/java/com/networknt/schema/Version201909.java index f8522f853..5114a806a 100644 --- a/src/main/java/com/networknt/schema/Version201909.java +++ b/src/main/java/com/networknt/schema/Version201909.java @@ -48,6 +48,7 @@ public JsonMetaSchema getInstance() { new NonValidationKeyword("deprecated"), new NonValidationKeyword("contentMediaType"), new NonValidationKeyword("contentEncoding"), + new NonValidationKeyword("contentSchema"), new NonValidationKeyword("examples"), new NonValidationKeyword("then"), new NonValidationKeyword("else") diff --git a/src/main/java/com/networknt/schema/Version202012.java b/src/main/java/com/networknt/schema/Version202012.java index 770a506ac..9816bb9a2 100644 --- a/src/main/java/com/networknt/schema/Version202012.java +++ b/src/main/java/com/networknt/schema/Version202012.java @@ -48,6 +48,7 @@ public JsonMetaSchema getInstance() { new NonValidationKeyword("deprecated"), new NonValidationKeyword("contentMediaType"), new NonValidationKeyword("contentEncoding"), + new NonValidationKeyword("contentSchema"), new NonValidationKeyword("examples"), new NonValidationKeyword("then"), new NonValidationKeyword("else"), diff --git a/src/main/java/com/networknt/schema/Version7.java b/src/main/java/com/networknt/schema/Version7.java index e8b24efc8..f5407d68b 100644 --- a/src/main/java/com/networknt/schema/Version7.java +++ b/src/main/java/com/networknt/schema/Version7.java @@ -26,8 +26,6 @@ public JsonMetaSchema getInstance() { new NonValidationKeyword("default"), new NonValidationKeyword("definitions"), new NonValidationKeyword("$comment"), - new NonValidationKeyword("contentMediaType"), - new NonValidationKeyword("contentEncoding"), new NonValidationKeyword("examples"), new NonValidationKeyword("then"), new NonValidationKeyword("else"), diff --git a/src/main/resources/jsv-messages.properties b/src/main/resources/jsv-messages.properties index bceacc396..374547c81 100644 --- a/src/main/resources/jsv-messages.properties +++ b/src/main/resources/jsv-messages.properties @@ -48,3 +48,5 @@ unionType = {0}: {1} found, but {2} is required uniqueItems = {0}: the items in the array must be unique uuid = {0}: {1} is an invalid {2} writeOnly = {0}: is a write-only field, it cannot appear in the data +contentEncoding = {0}: does not match content encoding {1} +contentMediaType = {0}: is not a content media type diff --git a/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java b/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java index eab976d5a..0bd25bbe7 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java @@ -77,7 +77,7 @@ private void disableV201909Tests() { } private void disableV7Tests() { - this.disabled.put(Paths.get("src/test/suite/tests/draft7/optional/content.json"), "Unsupported behavior"); + //this.disabled.put(Paths.get("src/test/suite/tests/draft7/optional/content.json"), "Unsupported behavior"); } private void disableV6Tests() { From 5b5a72dcdb29920ae4b7f5703cf461fea2cce122 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 24 Jan 2024 08:18:09 +0800 Subject: [PATCH 38/65] Update compatibility doc --- README.md | 4 +++- doc/compatibility.md | 53 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 99af87497..eb78978a9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ [![Javadocs](http://www.javadoc.io/badge/com.networknt/json-schema-validator.svg)](https://www.javadoc.io/doc/com.networknt/json-schema-validator) -This is a Java implementation of the [JSON Schema Core Draft v4, v6, v7, v2019-09 and v2020-12(partial)](http://json-schema.org/latest/json-schema-core.html) specification for JSON schema validation. In addition, it also works for OpenAPI 3.0 request/response validation with some [configuration flags](doc/config.md). For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The default JSON parser is the [Jackson](https://github.com/FasterXML/jackson) that is the most popular one. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design. +This is a Java implementation of the [JSON Schema Core Draft v4, v6, v7, v2019-09 and v2020-12](http://json-schema.org/latest/json-schema-core.html) specification for JSON schema validation. Information on the compatibility support for each version can be found [here](doc/compatibility.md). + +In addition, it also works for OpenAPI 3.0 request/response validation with some [configuration flags](doc/config.md). For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The default JSON parser is the [Jackson](https://github.com/FasterXML/jackson) that is the most popular one. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design. ## Why this library diff --git a/doc/compatibility.md b/doc/compatibility.md index 525a02a01..f5119484d 100644 --- a/doc/compatibility.md +++ b/doc/compatibility.md @@ -1,3 +1,13 @@ +## Compatibility with JSON Schema versions + +This implementation does not currently generate annotations. + +The `pattern` validator by default uses the JDK regular expression implementation which is not ECMA-262 compliant and is thus not compliant with the JSON Schema specification. The library can however be configured to use a ECMA-262 compliant regular expression implementation. + +### Known Issues +* The `anyOf` applicator currently returns immediately on matching a schema. This results in the `unevaluatedItems` and `unevaluatedProperties` keywords potentially returning an incorrect result as the rest of the schemas in the `anyOf` aren't processed. +* The `unevaluatedItems` keyword does not currently consider `contains`. + ### Legend @@ -8,7 +18,7 @@ | 🔴 | Not implemented | | 🚫 | Not defined | -### Compatibility with JSON Schema versions +### Keywords Support | Keyword | Draft 4 | Draft 6 | Draft 7 | Draft 2019-09 | Draft 2020-12 | |:---------------------------|:-------:|:-------:|:-------:|:-------------:|:-------------:| @@ -19,16 +29,16 @@ | $recursiveAnchor | 🚫 | 🚫 | 🚫 | 🟢 | 🚫 | | $recursiveRef | 🚫 | 🚫 | 🚫 | 🟢 | 🚫 | | $ref | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | -| $vocabulary | 🚫 | 🚫 | 🚫 | 🔴 | 🔴 | +| $vocabulary | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | | additionalItems | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | | additionalProperties | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | | allOf | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | | anyOf | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | | const | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | | contains | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | -| contentEncoding | 🚫 | 🚫 | 🔴 | 🔴 | 🔴 | -| contentMediaType | 🚫 | 🚫 | 🔴 | 🔴 | 🔴 | -| contentSchema | 🚫 | 🚫 | 🚫 | 🔴 | 🔴 | +| contentEncoding | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| contentMediaType | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| contentSchema | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | | definitions | 🟢 | 🟢 | 🟢 | 🚫 | 🚫 | | defs | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | | dependencies | 🟢 | 🟢 | 🟢 | 🚫 | 🚫 | @@ -67,7 +77,38 @@ | uniqueItems | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | | writeOnly | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | -### Semantic Validation (Format) +#### Content Encoding + +Since Draft 2019-09, the `contentEncoding` keyword does not generate assertions. As the implementation currently does not collect annotations this only generates assertions in Draft 7. + +#### Content Media Type + +Since Draft 2019-09, the `contentMediaType` keyword does not generate assertions. As the implementation currently does not collect annotations this only generates assertions in Draft 7. + +#### Content Schema + +The `contentSchema` keyword does not generate assertions. As the implementation currently does not collect annotations this doesn't do anything. + +#### Pattern + +By default the `pattern` keyword uses the JDK regular expression implementation validating regular expressions. + +This is not ECMA-262 compliant and is thus not compliant with the JSON Schema specification. This is however the more likely desired behavior as other logic will most likely be using the default JDK regular expression implementation to perform downstream processing. + +The library can be configured to use a ECMA-262 compliant regular expression validator which is implemented using [joni](https://github.com/jruby/joni). This can be configured by setting `setEcma262Validator` to `true`. + +### Format + +Since Draft 2019-09 the `format` keyword only generates annotations by default and does not generate assertions. + +This can be configured on a schema basis by using a meta schema with the appropriate vocabulary. + +| Version | Vocabulary | Value | +|:----------------------|---------------------------------------------------------------|-------------------| +| Draft 2019-09 | `https://json-schema.org/draft/2019-09/vocab/format` | `true` | +| Draft 2020-12 | `https://json-schema.org/draft/2020-12/vocab/format-assertion`| `true`/`false` | + +This behavior can be overridden to generate assertions on a per-execution basis by setting the `setFormatAssertionsEnabled` to `true`. | Format | Draft 4 | Draft 6 | Draft 7 | Draft 2019-09 | Draft 2020-12 | |:----------------------|:-------:|:-------:|:-------:|:-------------:|:-------------:| From 8acfd836ba97765f774890507c1a314eb2235f32 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:24:37 +0800 Subject: [PATCH 39/65] Refactor enumObject is failing --- .../java/com/networknt/schema/JsonSchema.java | 60 ++++++++++++++++--- .../networknt/schema/JsonSchemaFactory.java | 10 ++-- .../com/networknt/schema/RefValidator.java | 9 ++- .../com/networknt/schema/SchemaLocation.java | 4 ++ .../schema/uri/ClasspathSchemaLoader.java | 4 ++ .../com/networknt/schema/ExampleTest.java | 52 ++++++++++++++++ src/test/resources/schema/example-main.json | 18 ++++++ src/test/resources/schema/example-ref.json | 24 ++++++++ 8 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 src/test/java/com/networknt/schema/ExampleTest.java create mode 100644 src/test/resources/schema/example-main.json create mode 100644 src/test/resources/schema/example-ref.json diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index a2a193771..e55c9a84a 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -19,12 +19,14 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.CollectorContext.Scope; +import com.networknt.schema.SchemaLocation.Fragment; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner; import com.networknt.schema.walk.WalkListenerRunner; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.function.Consumer; @@ -54,17 +56,37 @@ public class JsonSchema extends BaseJsonValidator { static JsonSchema from(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { return new JsonSchema(validationContext, schemaLocation, evaluationPath, schemaNode, parent, suppressSubSchemaRetrieval); } - + + private boolean hasNoFragment(SchemaLocation schemaLocation) { + return this.schemaLocation.getFragment() == null || this.schemaLocation.getFragment().getNameCount() == 0; + } + private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { super(schemaLocation.resolve(validationContext.resolveSchemaId(schemaNode)), evaluationPath, schemaNode, parent, null, null, validationContext, suppressSubSchemaRetrieval); this.metaSchema = this.validationContext.getMetaSchema(); initializeConfig(); - this.id = this.validationContext.resolveSchemaId(this.schemaNode); - if (this.id != null) { + String id = this.validationContext.resolveSchemaId(this.schemaNode); + if (id != null) { + // In earlier drafts $id may contain an anchor fragment + // Note that json pointer fragments in $id are not allowed + if (hasNoFragment(schemaLocation)) { + this.id = id; + } else { + this.id = id; + } this.validationContext.getSchemaResources() - .putIfAbsent(this.schemaLocation != null ? this.schemaLocation.toString() : this.id, this); + .putIfAbsent(this.schemaLocation != null ? this.schemaLocation.toString() : id, this); + } else { + if (hasNoFragment(schemaLocation)) { + // No $id but there is no fragment and is thus a schema resource + this.id = this.schemaLocation.getAbsoluteIri() != null ? this.schemaLocation.getAbsoluteIri().toString() : ""; + this.validationContext.getSchemaResources() + .putIfAbsent(this.schemaLocation != null ? this.schemaLocation.toString() : this.id, this); + } else { + this.id = null; + } } String anchor = this.validationContext.getMetaSchema().readAnchor(this.schemaNode); if (anchor != null) { @@ -236,8 +258,18 @@ public JsonSchema getSubSchema(JsonNodePath fragment) { parent); parent = subSchema; } else { - throw new JsonSchemaException("Unable to find subschema " + fragment.toString() + " in " - + document.getSchemaLocation().toString()); + // In earlier drafts this can be because the parent is incorrect draft4\extra\classpath\schema.json + // This follows the old logic for handleNullNode + JsonSchema found = parent.findSchemaResourceRoot().fetchSubSchemaNode(this.validationContext); + if (found != null) { + found = found.getSubSchema(fragment); + } + if (found == null) { + throw new JsonSchemaException("Unable to find subschema " + fragment.toString() + " in " + + parent.getSchemaLocation().toString() + " at evaluation path " + + parent.getEvaluationPath().toString()); + } + return found; } } return subSchema; @@ -249,7 +281,21 @@ protected JsonNode getNode(Object propertyOrIndex) { if (propertyOrIndex instanceof Number) { value = node.get(((Number) propertyOrIndex).intValue()); } else { - value = node.get(propertyOrIndex.toString()); + // In the case of string this represents an escaped json pointer and thus does not reflect the property directly + String unescaped = propertyOrIndex.toString(); + if (unescaped.contains("~")) { + unescaped = unescaped.replace("~1", "/"); + unescaped = unescaped.replace("~0", "~"); + } + if (unescaped.contains("%")) { + try { + unescaped = URLDecoder.decode(unescaped, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + // Do nothing + } + } + + value = node.get(unescaped); } return value; } diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 576be0622..b90af715e 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -416,12 +416,11 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid final JsonMetaSchema jsonMetaSchema = getMetaSchemaOrDefault(schemaNode, config); JsonNodePath evaluationPath = new JsonNodePath(config.getPathType()); - JsonSchema jsonSchema; if (schemaUri.getFragment() == null || schemaUri.getFragment().getNameCount() == 0) { // Schema without fragment ValidationContext validationContext = new ValidationContext(jsonMetaSchema, this, config); - jsonSchema = doCreate(validationContext, schemaUri, evaluationPath, schemaNode, null, true /* retrieved via id, resolving will not change anything */); + return doCreate(validationContext, schemaUri, evaluationPath, schemaNode, null, true /* retrieved via id, resolving will not change anything */); } else { // Schema with fragment pointing to sub schema final ValidationContext validationContext = createValidationContext(schemaNode, config); @@ -429,10 +428,11 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid JsonSchema document = doCreate(validationContext, documentLocation, evaluationPath, schemaNode, null, false); return document.getRefSchema(schemaUri.getFragment()); } - return jsonSchema; } catch (IOException e) { - logger.error("Failed to load json schema from {}", schemaUri, e); - throw new JsonSchemaException(e); + logger.error("Failed to load json schema from {}", schemaUri.getAbsoluteIri(), e); + JsonSchemaException exception = new JsonSchemaException("Failed to load json schema from "+schemaUri.getAbsoluteIri()); + exception.initCause(e); + throw exception; } } diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 17ef67b2e..b9794a95c 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -144,9 +144,14 @@ private static JsonSchema getJsonSchema(JsonSchema parent, String refValueOriginal, JsonNodePath evaluationPath) { JsonNode node = parent.getRefSchemaNode(refValue); + JsonNodePath fragment = SchemaLocation.Fragment.of(refValue); + if (!refValue.startsWith("#/")) { + throw new IllegalArgumentException(refValue); + } if (node != null) { - return validationContext.getSchemaReferences().computeIfAbsent(refValueOriginal, key -> { - return getJsonSchema(node, parent, refValue, evaluationPath); + String schemaReference = resolve(parent, refValueOriginal); + return validationContext.getSchemaReferences().computeIfAbsent(schemaReference, key -> { + return parent.getSubSchema(fragment); }); } return null; diff --git a/src/main/java/com/networknt/schema/SchemaLocation.java b/src/main/java/com/networknt/schema/SchemaLocation.java index 8daf9c6b1..7e18678d1 100644 --- a/src/main/java/com/networknt/schema/SchemaLocation.java +++ b/src/main/java/com/networknt/schema/SchemaLocation.java @@ -233,6 +233,10 @@ public static JsonNodePath of(String fragmentString) { fragment = fragment.append(fragmentPart.toString()); } } + if (index == -1 && fragmentString.endsWith("/")) { + // Trailing / in fragment + fragment = fragment.append(""); + } return fragment; } diff --git a/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java b/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java index 8c7349bf9..6605a0dc2 100644 --- a/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java +++ b/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java @@ -15,6 +15,7 @@ */ package com.networknt.schema.uri; +import java.io.FileNotFoundException; import java.io.InputStream; import com.networknt.schema.AbsoluteIri; @@ -43,6 +44,9 @@ public InputStreamSource getSchema(AbsoluteIri absoluteIri) { if (result == null) { result = loader.getResourceAsStream(resource.substring(1)); } + if (result == null) { + throw new FileNotFoundException(absoluteIri.toString()); + } return result; }; } diff --git a/src/test/java/com/networknt/schema/ExampleTest.java b/src/test/java/com/networknt/schema/ExampleTest.java new file mode 100644 index 000000000..6b9f2229f --- /dev/null +++ b/src/test/java/com/networknt/schema/ExampleTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class ExampleTest { + @Test + public void example() throws Exception { + // This creates a schema factory that will use Draft 2012-12 as the default if $schema is not specified in the initial schema + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> { + builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:schema/")); + }); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of("https://www.example.org/example-main.json"), config); + String input = "{\r\n" + + " \"DriverProperties\": {\r\n" + + " \"CommonProperties\": {\r\n" + + " \"field2\": \"abc-def-xyz\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + // The example-main.json schema defines $schema with Draft 07 + assertEquals("https://json-schema.org/draft-07/schema", schema.getValidationContext().getMetaSchema().getUri()); + Set errors = schema.validate(JsonMapperFactory.getInstance().readTree(input)); + assertEquals(1, errors.size()); + + // The example-ref.json schema defines $schema with Draft 2019-09 + JsonSchema refSchema = schema.getValidationContext().getSchemaResources().get("https://www.example.org/example-ref.json#"); + assertEquals("https://json-schema.org/draft/2019-09/schema", refSchema.getValidationContext().getMetaSchema().getUri()); + } +} diff --git a/src/test/resources/schema/example-main.json b/src/test/resources/schema/example-main.json new file mode 100644 index 000000000..e7fa885b2 --- /dev/null +++ b/src/test/resources/schema/example-main.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "required" : ["DriverProperties"], + "properties": { + "DriverProperties": { + "type": "object", + "properties": { + "CommonProperties": { + "$ref": "example-ref.json#/definitions/DriverProperties" + } + }, + "required": ["CommonProperties"], + "additionalProperties": false + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/src/test/resources/schema/example-ref.json b/src/test/resources/schema/example-ref.json new file mode 100644 index 000000000..39dfc0b17 --- /dev/null +++ b/src/test/resources/schema/example-ref.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "additionalProperties": false, + "definitions": { + "DriverProperties": { + "type": "object", + "properties": { + "field1": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "field2": { + "type": "string", + "minLength": 1, + "maxLength": 512 + } + }, + "required": ["field1"], + "additionalProperties": false + } + } +} \ No newline at end of file From e946e918753621911ef750d197c069538165157c Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 24 Jan 2024 19:13:13 +0800 Subject: [PATCH 40/65] Fix --- .../com/networknt/schema/RefValidator.java | 21 +++++++++++++++++-- .../networknt/schema/utils/JsonNodeUtil.java | 8 +++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index b9794a95c..804900a60 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -111,7 +111,8 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val () -> parentSchema.findSchemaResourceRoot().fromRef(parentSchema, evaluationPath))); } return new JsonSchemaRef(new CachedSupplier<>( - () -> getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath))); + () -> getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath) + .fromRef(parentSchema, evaluationPath))); } private static void copySchemaResources(ValidationContext validationContext, JsonSchema schemaResource) { @@ -279,6 +280,22 @@ public void preloadJsonSchema() { } catch (RuntimeException e) { throw new JsonSchemaException(e); } - jsonSchema.initializeValidators(); + // Check for circular dependency + // Only one cycle is pre-loaded + // The rest of the cycles will load at execution time depending on the input + // data + SchemaLocation schemaLocation = jsonSchema.getSchemaLocation(); + JsonSchema check = jsonSchema; + boolean circularDependency = false; + while(check.getEvaluationParentSchema() != null) { + check = check.getEvaluationParentSchema(); + if (check.getSchemaLocation().equals(schemaLocation)) { + circularDependency = true; + break; + } + } + if(!circularDependency) { + jsonSchema.initializeValidators(); + } } } diff --git a/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java b/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java index fc403df5e..5143202b4 100644 --- a/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java +++ b/src/main/java/com/networknt/schema/utils/JsonNodeUtil.java @@ -163,18 +163,16 @@ private static boolean isEnumObjectSchema(JsonSchema jsonSchema) { // 3. The parent schema if refer from components, which means the corresponding enum object class would be generated JsonNode typeNode = null; JsonNode enumNode = null; - JsonNode refNode = null; + boolean refNode = false; if (jsonSchema != null) { if (jsonSchema.getSchemaNode() != null) { typeNode = jsonSchema.getSchemaNode().get(TYPE); enumNode = jsonSchema.getSchemaNode().get(ENUM); } - if (jsonSchema.getParentSchema() != null && jsonSchema.getParentSchema().getSchemaNode() != null) { - refNode = jsonSchema.getParentSchema().getSchemaNode().get(REF); - } + refNode = REF.equals(jsonSchema.getEvaluationPath().getElement(-1)); } - if (typeNode != null && enumNode != null && refNode != null) { + if (typeNode != null && enumNode != null && refNode) { return TypeFactory.getSchemaNodeType(typeNode) == JsonType.OBJECT && enumNode.isArray(); } return false; From 892f7bdfb6fa3fa48d53884712d1d3abe8a80002 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 24 Jan 2024 19:15:40 +0800 Subject: [PATCH 41/65] Refactor --- .../java/com/networknt/schema/JsonSchema.java | 1 - .../com/networknt/schema/RefValidator.java | 51 ------------------- .../schema/JsonSchemaTestSuiteTest.java | 3 +- 3 files changed, 1 insertion(+), 54 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index e55c9a84a..242d4f6a3 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.CollectorContext.Scope; -import com.networknt.schema.SchemaLocation.Fragment; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner; import com.networknt.schema.walk.WalkListenerRunner; diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 804900a60..42f93a8eb 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -146,9 +146,6 @@ private static JsonSchema getJsonSchema(JsonSchema parent, JsonNodePath evaluationPath) { JsonNode node = parent.getRefSchemaNode(refValue); JsonNodePath fragment = SchemaLocation.Fragment.of(refValue); - if (!refValue.startsWith("#/")) { - throw new IllegalArgumentException(refValue); - } if (node != null) { String schemaReference = resolve(parent, refValueOriginal); return validationContext.getSchemaReferences().computeIfAbsent(schemaReference, key -> { @@ -157,54 +154,6 @@ private static JsonSchema getJsonSchema(JsonSchema parent, } return null; } - - private static JsonSchema getJsonSchema(JsonNode node, JsonSchema parent, - String refValue, - JsonNodePath evaluationPath) { - if (node != null) { - SchemaLocation path = null; - JsonSchema currentParent = parent; - if (refValue.startsWith(REF_CURRENT)) { - // relative to document - path = new SchemaLocation(parent.schemaLocation.getAbsoluteIri(), - new JsonNodePath(PathType.JSON_POINTER)); - // Attempt to get subschema node - String[] refParts = refValue.split("/"); - if (refParts.length > 3) { - String[] subschemaParts = Arrays.copyOf(refParts, refParts.length - 2); - JsonNode subschemaNode = parent.getRefSchemaNode(String.join("/", subschemaParts)); - String id = parent.getValidationContext().resolveSchemaId(subschemaNode); - if (id != null) { - if (id.contains(":")) { - // absolute - path = SchemaLocation.of(id); - } else { - // relative - String absoluteUri = path.getAbsoluteIri().resolve(id).toString(); - path = SchemaLocation.of(absoluteUri); - } - } - } - String[] parts = refValue.split("/"); - for (int x = 1; x < parts.length; x++) { - path = path.append(parts[x]); - } - } else if(refValue.contains(":")) { - // absolute - path = SchemaLocation.of(refValue); - } else { - // relative to lexical root - String id = parent.findSchemaResourceRoot().getId(); - path = SchemaLocation.of(id); - String[] parts = refValue.split("/"); - for (int x = 1; x < parts.length; x++) { - path = path.append(parts[x]); - } - } - return parent.getValidationContext().newSchema(path, evaluationPath, node, currentParent); - } - throw null; - } @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { diff --git a/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java b/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java index 0bd25bbe7..8528b12b6 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java @@ -1,7 +1,6 @@ package com.networknt.schema; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -77,7 +76,7 @@ private void disableV201909Tests() { } private void disableV7Tests() { - //this.disabled.put(Paths.get("src/test/suite/tests/draft7/optional/content.json"), "Unsupported behavior"); + // nothing here } private void disableV6Tests() { From cf566b78a881f0a57ed583a741fdb2ba82313526 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 03:26:27 +0800 Subject: [PATCH 42/65] Refactor package --- .../java/com/networknt/schema/I18nSupport.java | 18 ------------------ .../networknt/schema/JsonSchemaFactory.java | 3 ++- .../ClasspathSchemaLoader.java | 2 +- .../{uri => resource}/DefaultSchemaLoader.java | 2 +- .../{uri => resource}/InputStreamSource.java | 2 +- .../{uri => resource}/MapSchemaLoader.java | 2 +- .../{uri => resource}/MapSchemaMapper.java | 2 +- .../{uri => resource}/PrefixSchemaMapper.java | 2 +- .../schema/{uri => resource}/SchemaLoader.java | 2 +- .../{uri => resource}/SchemaLoaders.java | 2 +- .../schema/{uri => resource}/SchemaMapper.java | 2 +- .../{uri => resource}/SchemaMappers.java | 2 +- .../{uri => resource}/UriSchemaLoader.java | 2 +- .../com/networknt/schema/urn/URNFactory.java | 13 ------------- .../com/networknt/schema/CustomUriTest.java | 4 ++-- .../schema/JsonSchemaFactoryUriCacheTest.java | 5 +++-- .../com/networknt/schema/UriMappingTest.java | 5 +++-- 17 files changed, 21 insertions(+), 49 deletions(-) delete mode 100644 src/main/java/com/networknt/schema/I18nSupport.java rename src/main/java/com/networknt/schema/{uri => resource}/ClasspathSchemaLoader.java (95%) rename src/main/java/com/networknt/schema/{uri => resource}/DefaultSchemaLoader.java (94%) rename src/main/java/com/networknt/schema/{uri => resource}/InputStreamSource.java (92%) rename src/main/java/com/networknt/schema/{uri => resource}/MapSchemaLoader.java (92%) rename src/main/java/com/networknt/schema/{uri => resource}/MapSchemaMapper.java (91%) rename src/main/java/com/networknt/schema/{uri => resource}/PrefixSchemaMapper.java (92%) rename src/main/java/com/networknt/schema/{uri => resource}/SchemaLoader.java (93%) rename src/main/java/com/networknt/schema/{uri => resource}/SchemaLoaders.java (95%) rename src/main/java/com/networknt/schema/{uri => resource}/SchemaMapper.java (93%) rename src/main/java/com/networknt/schema/{uri => resource}/SchemaMappers.java (95%) rename src/main/java/com/networknt/schema/{uri => resource}/UriSchemaLoader.java (96%) delete mode 100644 src/main/java/com/networknt/schema/urn/URNFactory.java diff --git a/src/main/java/com/networknt/schema/I18nSupport.java b/src/main/java/com/networknt/schema/I18nSupport.java deleted file mode 100644 index e77e07436..000000000 --- a/src/main/java/com/networknt/schema/I18nSupport.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.networknt.schema; - -import java.util.Locale; -import java.util.ResourceBundle; - -/** - * Created by leaves chen leaves615@gmail.com on 2021/8/23. - * - * @author leaves chen leaves615@gmail.com - */ -@Deprecated -public class I18nSupport { - - public static final String DEFAULT_BUNDLE_BASE_NAME = "jsv-messages"; - public static final Locale DEFAULT_LOCALE = Locale.getDefault(); - public static final ResourceBundle DEFAULT_RESOURCE_BUNDLE = ResourceBundle.getBundle(DEFAULT_BUNDLE_BASE_NAME, DEFAULT_LOCALE); - -} diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index b90af715e..91dc726af 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -19,7 +19,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.SpecVersion.VersionFlag; -import com.networknt.schema.uri.*; +import com.networknt.schema.resource.*; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java b/src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java similarity index 95% rename from src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java rename to src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java index 6605a0dc2..366c15e3d 100644 --- a/src/main/java/com/networknt/schema/uri/ClasspathSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.networknt.schema.uri; +package com.networknt.schema.resource; import java.io.FileNotFoundException; import java.io.InputStream; diff --git a/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java b/src/main/java/com/networknt/schema/resource/DefaultSchemaLoader.java similarity index 94% rename from src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java rename to src/main/java/com/networknt/schema/resource/DefaultSchemaLoader.java index 3ff132524..e31b7930d 100644 --- a/src/main/java/com/networknt/schema/uri/DefaultSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/DefaultSchemaLoader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.networknt.schema.uri; +package com.networknt.schema.resource; import java.util.List; diff --git a/src/main/java/com/networknt/schema/uri/InputStreamSource.java b/src/main/java/com/networknt/schema/resource/InputStreamSource.java similarity index 92% rename from src/main/java/com/networknt/schema/uri/InputStreamSource.java rename to src/main/java/com/networknt/schema/resource/InputStreamSource.java index 149cb8235..fb2154213 100644 --- a/src/main/java/com/networknt/schema/uri/InputStreamSource.java +++ b/src/main/java/com/networknt/schema/resource/InputStreamSource.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.networknt.schema.uri; +package com.networknt.schema.resource; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/com/networknt/schema/uri/MapSchemaLoader.java b/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java similarity index 92% rename from src/main/java/com/networknt/schema/uri/MapSchemaLoader.java rename to src/main/java/com/networknt/schema/resource/MapSchemaLoader.java index d6b036d19..86b4633ac 100644 --- a/src/main/java/com/networknt/schema/uri/MapSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java @@ -1,4 +1,4 @@ -package com.networknt.schema.uri; +package com.networknt.schema.resource; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; diff --git a/src/main/java/com/networknt/schema/uri/MapSchemaMapper.java b/src/main/java/com/networknt/schema/resource/MapSchemaMapper.java similarity index 91% rename from src/main/java/com/networknt/schema/uri/MapSchemaMapper.java rename to src/main/java/com/networknt/schema/resource/MapSchemaMapper.java index 25916374b..1e7b34f2b 100644 --- a/src/main/java/com/networknt/schema/uri/MapSchemaMapper.java +++ b/src/main/java/com/networknt/schema/resource/MapSchemaMapper.java @@ -1,4 +1,4 @@ -package com.networknt.schema.uri; +package com.networknt.schema.resource; import java.util.Map; import java.util.function.Function; diff --git a/src/main/java/com/networknt/schema/uri/PrefixSchemaMapper.java b/src/main/java/com/networknt/schema/resource/PrefixSchemaMapper.java similarity index 92% rename from src/main/java/com/networknt/schema/uri/PrefixSchemaMapper.java rename to src/main/java/com/networknt/schema/resource/PrefixSchemaMapper.java index aa7eee080..46f4f805b 100644 --- a/src/main/java/com/networknt/schema/uri/PrefixSchemaMapper.java +++ b/src/main/java/com/networknt/schema/resource/PrefixSchemaMapper.java @@ -1,4 +1,4 @@ -package com.networknt.schema.uri; +package com.networknt.schema.resource; import com.networknt.schema.AbsoluteIri; diff --git a/src/main/java/com/networknt/schema/uri/SchemaLoader.java b/src/main/java/com/networknt/schema/resource/SchemaLoader.java similarity index 93% rename from src/main/java/com/networknt/schema/uri/SchemaLoader.java rename to src/main/java/com/networknt/schema/resource/SchemaLoader.java index 5e142cdba..86aa0f748 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/SchemaLoader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.networknt.schema.uri; +package com.networknt.schema.resource; import com.networknt.schema.AbsoluteIri; diff --git a/src/main/java/com/networknt/schema/uri/SchemaLoaders.java b/src/main/java/com/networknt/schema/resource/SchemaLoaders.java similarity index 95% rename from src/main/java/com/networknt/schema/uri/SchemaLoaders.java rename to src/main/java/com/networknt/schema/resource/SchemaLoaders.java index f1b26da88..b5b9288f7 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaLoaders.java +++ b/src/main/java/com/networknt/schema/resource/SchemaLoaders.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.networknt.schema.uri; +package com.networknt.schema.resource; import java.util.ArrayList; import java.util.Collection; diff --git a/src/main/java/com/networknt/schema/uri/SchemaMapper.java b/src/main/java/com/networknt/schema/resource/SchemaMapper.java similarity index 93% rename from src/main/java/com/networknt/schema/uri/SchemaMapper.java rename to src/main/java/com/networknt/schema/resource/SchemaMapper.java index 8cf3bca81..be6813a16 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaMapper.java +++ b/src/main/java/com/networknt/schema/resource/SchemaMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.networknt.schema.uri; +package com.networknt.schema.resource; import com.networknt.schema.AbsoluteIri; diff --git a/src/main/java/com/networknt/schema/uri/SchemaMappers.java b/src/main/java/com/networknt/schema/resource/SchemaMappers.java similarity index 95% rename from src/main/java/com/networknt/schema/uri/SchemaMappers.java rename to src/main/java/com/networknt/schema/resource/SchemaMappers.java index 317eb1199..2b0905943 100644 --- a/src/main/java/com/networknt/schema/uri/SchemaMappers.java +++ b/src/main/java/com/networknt/schema/resource/SchemaMappers.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.networknt.schema.uri; +package com.networknt.schema.resource; import java.util.ArrayList; import java.util.Collection; diff --git a/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java b/src/main/java/com/networknt/schema/resource/UriSchemaLoader.java similarity index 96% rename from src/main/java/com/networknt/schema/uri/UriSchemaLoader.java rename to src/main/java/com/networknt/schema/resource/UriSchemaLoader.java index f1842e02c..7a7aaeb9c 100644 --- a/src/main/java/com/networknt/schema/uri/UriSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/UriSchemaLoader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.networknt.schema.uri; +package com.networknt.schema.resource; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/com/networknt/schema/urn/URNFactory.java b/src/main/java/com/networknt/schema/urn/URNFactory.java deleted file mode 100644 index 5f5456ac6..000000000 --- a/src/main/java/com/networknt/schema/urn/URNFactory.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.networknt.schema.urn; - -import java.net.URI; - -public interface URNFactory -{ - /** - * @param urn Some urn string. - * @return The converted {@link URI}. - * @throws IllegalArgumentException if there was a problem creating the {@link URI} with the given data. - */ - URI create(String urn); -} diff --git a/src/test/java/com/networknt/schema/CustomUriTest.java b/src/test/java/com/networknt/schema/CustomUriTest.java index d796887be..a39e15810 100644 --- a/src/test/java/com/networknt/schema/CustomUriTest.java +++ b/src/test/java/com/networknt/schema/CustomUriTest.java @@ -1,8 +1,8 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.uri.InputStreamSource; -import com.networknt.schema.uri.SchemaLoader; +import com.networknt.schema.resource.InputStreamSource; +import com.networknt.schema.resource.SchemaLoader; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java index f6660f2d3..27e464fe0 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java @@ -2,8 +2,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.uri.InputStreamSource; -import com.networknt.schema.uri.SchemaLoader; +import com.networknt.schema.resource.InputStreamSource; +import com.networknt.schema.resource.SchemaLoader; + import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; diff --git a/src/test/java/com/networknt/schema/UriMappingTest.java b/src/test/java/com/networknt/schema/UriMappingTest.java index c6ac2286a..c0566344e 100644 --- a/src/test/java/com/networknt/schema/UriMappingTest.java +++ b/src/test/java/com/networknt/schema/UriMappingTest.java @@ -18,8 +18,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.JsonSchemaFactory.Builder; -import com.networknt.schema.uri.SchemaMapper; -import com.networknt.schema.uri.MapSchemaMapper; +import com.networknt.schema.resource.MapSchemaMapper; +import com.networknt.schema.resource.SchemaMapper; + import org.junit.jupiter.api.Test; import java.io.FileNotFoundException; From 7ea76aede3fd7332f0995585e8be915b639d800d Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 03:40:50 +0800 Subject: [PATCH 43/65] Refactor config --- .../networknt/schema/BaseJsonValidator.java | 2 +- .../com/networknt/schema/ExecutionConfig.java | 4 ++ ...r.java => ExecutionContextCustomizer.java} | 2 +- .../java/com/networknt/schema/JsonSchema.java | 19 +++++---- .../schema/SchemaValidatorsConfig.java | 41 ++++++++++++++++--- 5 files changed, 53 insertions(+), 15 deletions(-) rename src/main/java/com/networknt/schema/{ExecutionCustomizer.java => ExecutionContextCustomizer.java} (93%) diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 9b48b7936..ff699860a 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -295,7 +295,7 @@ public T validate(ExecutionContext executionContext, JsonNode node, OutputFo * @return the result */ public T validate(ExecutionContext executionContext, JsonNode node, OutputFormat format, - ExecutionCustomizer executionCustomizer) { + ExecutionContextCustomizer executionCustomizer) { format.customize(executionContext, this.validationContext); if (executionCustomizer != null) { executionCustomizer.customize(executionContext, validationContext); diff --git a/src/main/java/com/networknt/schema/ExecutionConfig.java b/src/main/java/com/networknt/schema/ExecutionConfig.java index a7de8ed48..594e3e88d 100644 --- a/src/main/java/com/networknt/schema/ExecutionConfig.java +++ b/src/main/java/com/networknt/schema/ExecutionConfig.java @@ -26,6 +26,10 @@ public class ExecutionConfig { private Locale locale = Locale.ROOT; private Predicate annotationAllowedPredicate = (keyword) -> true; + + /** + * Since Draft 2019-09 format assertions are not enabled by default. + */ private Boolean formatAssertionsEnabled = null; public Locale getLocale() { diff --git a/src/main/java/com/networknt/schema/ExecutionCustomizer.java b/src/main/java/com/networknt/schema/ExecutionContextCustomizer.java similarity index 93% rename from src/main/java/com/networknt/schema/ExecutionCustomizer.java rename to src/main/java/com/networknt/schema/ExecutionContextCustomizer.java index c24038b8d..edaa94953 100644 --- a/src/main/java/com/networknt/schema/ExecutionCustomizer.java +++ b/src/main/java/com/networknt/schema/ExecutionContextCustomizer.java @@ -20,7 +20,7 @@ * Customize the execution context before validation. */ @FunctionalInterface -public interface ExecutionCustomizer { +public interface ExecutionContextCustomizer { /** * Customize the execution context before validation. *

diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 242d4f6a3..d08ce4b84 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -575,7 +575,7 @@ public Set validate(JsonNode rootNode) { * @param executionCustomizer the execution customizer * @return the assertions */ - public Set validate(JsonNode rootNode, ExecutionCustomizer executionCustomizer) { + public Set validate(JsonNode rootNode, ExecutionContextCustomizer executionCustomizer) { return validate(rootNode, OutputFormat.DEFAULT, executionCustomizer); } @@ -612,7 +612,7 @@ public Set validate(JsonNode rootNode, Consumer T validate(JsonNode rootNode, OutputFormat format) { - return validate(rootNode, format, (ExecutionCustomizer) null); + return validate(rootNode, format, (ExecutionContextCustomizer) null); } /** @@ -631,7 +631,7 @@ public T validate(JsonNode rootNode, OutputFormat format) { * @param executionCustomizer the execution customizer * @return the result */ - public T validate(JsonNode rootNode, OutputFormat format, ExecutionCustomizer executionCustomizer) { + public T validate(JsonNode rootNode, OutputFormat format, ExecutionContextCustomizer executionCustomizer) { return validate(createExecutionContext(), rootNode, format, executionCustomizer); } @@ -852,13 +852,18 @@ public boolean isRecursiveAnchor() { */ public ExecutionContext createExecutionContext() { SchemaValidatorsConfig config = validationContext.getConfig(); - if(config.getExecutionContextSupplier() != null) { - return config.getExecutionContextSupplier().get(); - } CollectorContext collectorContext = new CollectorContext(config.isUnevaluatedItemsAnalysisDisabled(), config.isUnevaluatedPropertiesAnalysisDisabled()); + + // Copy execution config defaults from validation config ExecutionConfig executionConfig = new ExecutionConfig(); executionConfig.setLocale(config.getLocale()); - return new ExecutionContext(executionConfig, collectorContext); + executionConfig.setFormatAssertionsEnabled(config.getFormatAssertionsEnabled()); + + ExecutionContext executionContext = new ExecutionContext(executionConfig, collectorContext); + if(config.getExecutionContextCustomizer() != null) { + config.getExecutionContextCustomizer().customize(executionContext, validationContext); + } + return executionContext; } } diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index ee1bb9ed7..22f9f9c62 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -27,7 +27,6 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.function.Supplier; public class SchemaValidatorsConfig { @@ -123,7 +122,7 @@ public class SchemaValidatorsConfig { private final List itemWalkListeners = new ArrayList<>(); - private Supplier executionContextSupplier; + private ExecutionContextCustomizer executionContextCustomizer; private boolean loadCollectors = true; @@ -137,6 +136,11 @@ public class SchemaValidatorsConfig { */ private MessageSource messageSource; + /** + * Since Draft 2019-09 format assertions are not enabled by default. + */ + private Boolean formatAssertionsEnabled = null; + /************************ START OF UNEVALUATED CHECKS **********************************/ // These are costly in terms of performance so we provide a way to disable them. @@ -325,12 +329,12 @@ public List getArrayItemWalkListeners() { public SchemaValidatorsConfig() { } - public Supplier getExecutionContextSupplier() { - return this.executionContextSupplier; + public ExecutionContextCustomizer getExecutionContextCustomizer() { + return this.executionContextCustomizer; } - public void setExecutionContextSupplier(Supplier executionContextSupplier) { - this.executionContextSupplier = executionContextSupplier; + public void setExecutionContextCustomizer(ExecutionContextCustomizer executionContextCustomizer) { + this.executionContextCustomizer = executionContextCustomizer; } public boolean isLosslessNarrowing() { @@ -516,4 +520,29 @@ public MessageSource getMessageSource() { public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } + + /** + * Gets the format assertion enabled flag. + *

+ * This defaults to null meaning that it will follow the defaults of the + * specification. + *

+ * Since draft 2019-09 this will default to false unless enabled by using the + * $vocabulary keyword. + * + * @return the format assertions enabled flag + */ + public Boolean getFormatAssertionsEnabled() { + return formatAssertionsEnabled; + } + + /** + * Sets the format assertion enabled flag. + * + * @param formatAssertionsEnabled the format assertions enabled flag + */ + public void setFormatAssertionsEnabled(Boolean formatAssertionsEnabled) { + this.formatAssertionsEnabled = formatAssertionsEnabled; + } + } From fe24f0bb7a7480ed56e578deb71dc9d329eb5cfb Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 03:50:17 +0800 Subject: [PATCH 44/65] validation context set in constructor --- src/main/java/com/networknt/schema/AllOfValidator.java | 1 - src/main/java/com/networknt/schema/AnyOfValidator.java | 1 - src/main/java/com/networknt/schema/BaseJsonValidator.java | 2 +- src/main/java/com/networknt/schema/EnumValidator.java | 1 - .../java/com/networknt/schema/ExclusiveMaximumValidator.java | 1 - .../java/com/networknt/schema/ExclusiveMinimumValidator.java | 1 - src/main/java/com/networknt/schema/ItemsValidator.java | 2 -- src/main/java/com/networknt/schema/ItemsValidator202012.java | 2 -- src/main/java/com/networknt/schema/MaxItemsValidator.java | 1 - src/main/java/com/networknt/schema/MaxLengthValidator.java | 1 - src/main/java/com/networknt/schema/MaximumValidator.java | 1 - src/main/java/com/networknt/schema/MinItemsValidator.java | 1 - src/main/java/com/networknt/schema/MinimumValidator.java | 1 - src/main/java/com/networknt/schema/PatternValidator.java | 1 - src/main/java/com/networknt/schema/PrefixItemsValidator.java | 2 -- src/main/java/com/networknt/schema/PropertiesValidator.java | 1 - src/main/java/com/networknt/schema/TypeValidator.java | 1 - src/main/java/com/networknt/schema/UnionTypeValidator.java | 1 - .../com/networknt/schema/format/BaseFormatJsonValidator.java | 2 +- .../java/com/networknt/schema/format/DateTimeValidator.java | 1 - 20 files changed, 2 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index 415e8e958..cf1dc2bc0 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -32,7 +32,6 @@ public class AllOfValidator extends BaseJsonValidator { public AllOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ALL_OF, validationContext); - this.validationContext = validationContext; int size = schemaNode.size(); for (int i = 0; i < size; i++) { this.schemas.add(validationContext.newSchema(schemaLocation.append(i), evaluationPath.append(i), diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index f3828cc1d..df735200f 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -34,7 +34,6 @@ public class AnyOfValidator extends BaseJsonValidator { public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext); - this.validationContext = validationContext; int size = schemaNode.size(); for (int i = 0; i < size; i++) { this.schemas.add(validationContext.newSchema(schemaLocation.append(i), evaluationPath.append(i), diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index ff699860a..4fbd8ecfa 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -32,7 +32,7 @@ public abstract class BaseJsonValidator extends ValidationMessageHandler impleme protected final ApplyDefaultsStrategy applyDefaultsStrategy; private final PathType pathType; - protected JsonNode schemaNode; + protected final JsonNode schemaNode; protected ValidationContext validationContext; diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java index adbde1a55..eb006b584 100644 --- a/src/main/java/com/networknt/schema/EnumValidator.java +++ b/src/main/java/com/networknt/schema/EnumValidator.java @@ -36,7 +36,6 @@ public class EnumValidator extends BaseJsonValidator implements JsonValidator { public EnumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ENUM, validationContext); - this.validationContext = validationContext; if (schemaNode != null && schemaNode.isArray()) { nodes = new HashSet(); StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java index 12779bd37..b67894600 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java @@ -34,7 +34,6 @@ public class ExclusiveMaximumValidator extends BaseJsonValidator { public ExclusiveMaximumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.EXCLUSIVE_MAXIMUM, validationContext); - this.validationContext = validationContext; if (!schemaNode.isNumber()) { throw new JsonSchemaException("exclusiveMaximum value is not a number"); } diff --git a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java index 879bbbb41..db0655330 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java @@ -38,7 +38,6 @@ public class ExclusiveMinimumValidator extends BaseJsonValidator { public ExclusiveMinimumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.EXCLUSIVE_MINIMUM, validationContext); - this.validationContext = validationContext; if (!schemaNode.isNumber()) { throw new JsonSchemaException("exclusiveMinimum value is not a number"); } diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 98cce6a85..822406486 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -66,8 +66,6 @@ public ItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath } this.arrayItemWalkListenerRunner = new DefaultItemWalkListenerRunner(validationContext.getConfig().getArrayItemWalkListeners()); - this.validationContext = validationContext; - this.schema = foundSchema; this.additionalSchema = foundAdditionalSchema; } diff --git a/src/main/java/com/networknt/schema/ItemsValidator202012.java b/src/main/java/com/networknt/schema/ItemsValidator202012.java index 7deb96937..6f770a4ff 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator202012.java +++ b/src/main/java/com/networknt/schema/ItemsValidator202012.java @@ -52,8 +52,6 @@ public ItemsValidator202012(SchemaLocation schemaLocation, JsonNodePath evaluati } this.arrayItemWalkListenerRunner = new DefaultItemWalkListenerRunner(validationContext.getConfig().getArrayItemWalkListeners()); - - this.validationContext = validationContext; } @Override diff --git a/src/main/java/com/networknt/schema/MaxItemsValidator.java b/src/main/java/com/networknt/schema/MaxItemsValidator.java index 46db032c5..63f6ccca5 100644 --- a/src/main/java/com/networknt/schema/MaxItemsValidator.java +++ b/src/main/java/com/networknt/schema/MaxItemsValidator.java @@ -35,7 +35,6 @@ public MaxItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP if (schemaNode.canConvertToExactIntegral()) { max = schemaNode.intValue(); } - this.validationContext = validationContext; } public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { diff --git a/src/main/java/com/networknt/schema/MaxLengthValidator.java b/src/main/java/com/networknt/schema/MaxLengthValidator.java index 55535418e..b93be3137 100644 --- a/src/main/java/com/networknt/schema/MaxLengthValidator.java +++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java @@ -34,7 +34,6 @@ public MaxLengthValidator(SchemaLocation schemaLocation, JsonNodePath evaluation if (schemaNode != null && schemaNode.canConvertToExactIntegral()) { maxLength = schemaNode.intValue(); } - this.validationContext = validationContext; } public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java index e05e53fa6..3bc086073 100644 --- a/src/main/java/com/networknt/schema/MaximumValidator.java +++ b/src/main/java/com/networknt/schema/MaximumValidator.java @@ -38,7 +38,6 @@ public class MaximumValidator extends BaseJsonValidator { public MaximumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MAXIMUM, validationContext); - this.validationContext = validationContext; if (!schemaNode.isNumber()) { throw new JsonSchemaException("maximum value is not a number"); } diff --git a/src/main/java/com/networknt/schema/MinItemsValidator.java b/src/main/java/com/networknt/schema/MinItemsValidator.java index e7811c8a9..969b399cb 100644 --- a/src/main/java/com/networknt/schema/MinItemsValidator.java +++ b/src/main/java/com/networknt/schema/MinItemsValidator.java @@ -33,7 +33,6 @@ public MinItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP if (schemaNode.canConvertToExactIntegral()) { min = schemaNode.intValue(); } - this.validationContext = validationContext; } public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java index 1b91da2ff..27ff40253 100644 --- a/src/main/java/com/networknt/schema/MinimumValidator.java +++ b/src/main/java/com/networknt/schema/MinimumValidator.java @@ -109,7 +109,6 @@ public String thresholdValue() { } }; } - this.validationContext = validationContext; } public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { diff --git a/src/main/java/com/networknt/schema/PatternValidator.java b/src/main/java/com/networknt/schema/PatternValidator.java index c29fe086b..71247cf95 100644 --- a/src/main/java/com/networknt/schema/PatternValidator.java +++ b/src/main/java/com/networknt/schema/PatternValidator.java @@ -41,7 +41,6 @@ public PatternValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPa logger.error("Failed to compile pattern '{}': {}", this.pattern, e.getMessage()); throw e; } - this.validationContext = validationContext; } private boolean matches(String value) { diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java index 4670f11bb..dc2b00a55 100644 --- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java @@ -50,8 +50,6 @@ public PrefixItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluati } this.arrayItemWalkListenerRunner = new DefaultItemWalkListenerRunner(validationContext.getConfig().getArrayItemWalkListeners()); - - this.validationContext = validationContext; } @Override diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index 67ffcb34f..cd6534f74 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -33,7 +33,6 @@ public class PropertiesValidator extends BaseJsonValidator { public PropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PROPERTIES, validationContext); - this.validationContext = validationContext; for (Iterator it = schemaNode.fieldNames(); it.hasNext(); ) { String pname = it.next(); this.schemas.put(pname, validationContext.newSchema(schemaLocation.append(pname), diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index ce9930e41..d780c80f2 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -34,7 +34,6 @@ public TypeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.TYPE, validationContext); this.schemaType = TypeFactory.getSchemaNodeType(schemaNode); this.parentSchema = parentSchema; - this.validationContext = validationContext; if (this.schemaType == JsonType.UNION) { this.unionTypeValidator = new UnionTypeValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext); } diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index 11a566267..e23b316ca 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -34,7 +34,6 @@ public class UnionTypeValidator extends BaseJsonValidator implements JsonValidat public UnionTypeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.UNION_TYPE, validationContext); - this.validationContext = validationContext; StringBuilder errorBuilder = new StringBuilder(); String sep = ""; diff --git a/src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java b/src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java index 948729502..80d985246 100644 --- a/src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java +++ b/src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java @@ -17,7 +17,7 @@ public abstract class BaseFormatJsonValidator extends BaseJsonValidator { public BaseFormatJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidatorTypeCode validatorType, ValidationContext validationContext) { - super(schemaLocation, evaluationPath, schemaNode, parentSchema, validatorType,validationContext); + super(schemaLocation, evaluationPath, schemaNode, parentSchema, validatorType, validationContext); VersionFlag specification = this.validationContext.getMetaSchema().getSpecification(); if (specification == null || specification.getVersionFlagValue() < VersionFlag.V201909.getVersionFlagValue()) { assertionsEnabled = true; diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index d3daea685..5e062b2d8 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -41,7 +41,6 @@ public class DateTimeValidator extends BaseFormatJsonValidator { public DateTimeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, ValidatorTypeCode type) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, type, validationContext); - this.validationContext = validationContext; } @Override From 872aa9d6afe26f5cc5eaf3043381609b724c8bca Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 04:03:50 +0800 Subject: [PATCH 45/65] Add javadoc --- .../com/networknt/schema/BaseJsonValidator.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 4fbd8ecfa..8a30144fa 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -252,10 +252,25 @@ public JsonNode getSchemaNode() { return this.schemaNode; } + /** + * Gets the parent schema. + *

+ * This is the lexical parent schema. + * + * @return the parent schema + */ public JsonSchema getParentSchema() { return this.parentSchema; } + /** + * Gets the evaluation parent schema. + *

+ * This is the dynamic parent schema when following references. + * + * @see JsonSchema#fromRef(JsonSchema, JsonNodePath) + * @return the evaluation parent schema + */ public JsonSchema getEvaluationParentSchema() { if (this.evaluationParentSchema != null) { return this.evaluationParentSchema; From 3c6fa2a55fab28a2de59e3353d5ae7577dac97ad Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 09:29:07 +0800 Subject: [PATCH 46/65] Only normalize standard json schema dialects --- .../networknt/schema/JsonSchemaFactory.java | 40 ++++++++++++---- .../java/com/networknt/schema/SchemaId.java | 46 +++++++++++++++++++ .../com/networknt/schema/SpecVersion.java | 10 ++-- .../com/networknt/schema/Version201909.java | 2 +- .../com/networknt/schema/Version202012.java | 2 +- .../java/com/networknt/schema/Version4.java | 4 +- .../java/com/networknt/schema/Version6.java | 2 +- .../java/com/networknt/schema/Version7.java | 2 +- .../schema/DependentRequiredTest.java | 2 +- .../com/networknt/schema/ExampleTest.java | 4 +- .../com/networknt/schema/Issue314Test.java | 2 +- .../schema/JsonSchemaFactoryUriCacheTest.java | 4 +- .../java/com/networknt/schema/RefTest.java | 8 ++-- .../schema/UnknownMetaSchemaTest.java | 2 +- src/test/resources/draft2019-09/issue375.json | 2 +- src/test/resources/schema/example-main.json | 2 +- .../resources/schema/issue575-2019-09.json | 2 +- 17 files changed, 103 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/networknt/schema/SchemaId.java diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 91dc726af..c1a78d269 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -549,14 +549,38 @@ private boolean isYaml(final SchemaLocation schemaUri) { return (".yml".equals(extension) || ".yaml".equals(extension)); } - static protected String normalizeMetaSchemaUri(String u) { - try { - URI uri = new URI(u); - URI newUri = new URI("https", uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null); - - return newUri.toString(); - } catch (URISyntaxException e) { - throw new JsonSchemaException("Wrong MetaSchema URI: " + u); + /** + * Normalizes the standard JSON schema dialects. + *

+ * This should not normalize any other unrecognized dialects. + * + * @param id the $schema identifier + * @return the normalized uri + */ + static protected String normalizeMetaSchemaUri(String id) { + boolean found = false; + for (VersionFlag flag : SpecVersion.VersionFlag.values()) { + if(flag.getId().equals(id)) { + found = true; + break; + } + } + if (!found) { + if (id.contains("://json-schema.org/draft")) { + // unnormalized $schema + if (id.contains("/draft-07/")) { + id = SchemaId.V7; + } else if (id.contains("/draft/2019-09/")) { + id = SchemaId.V201909; + } else if (id.contains("/draft/2020-12/")) { + id = SchemaId.V202012; + } else if (id.contains("/draft-04/")) { + id = SchemaId.V4; + } else if (id.contains("/draft-06/")) { + id = SchemaId.V6; + } + } } + return id; } } diff --git a/src/main/java/com/networknt/schema/SchemaId.java b/src/main/java/com/networknt/schema/SchemaId.java new file mode 100644 index 000000000..6f7b84fec --- /dev/null +++ b/src/main/java/com/networknt/schema/SchemaId.java @@ -0,0 +1,46 @@ +/* + * 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; + +/** + * Schema Identifier used in $schema. + */ +public class SchemaId { + /** + * Draft 4. + */ + public static final String V4 = "http://json-schema.org/draft-04/schema#"; + + /** + * Draft 6. + */ + public static final String V6 = "http://json-schema.org/draft-06/schema#"; + + /** + * Draft 7. + */ + public static final String V7 = "http://json-schema.org/draft-07/schema#"; + + /** + * Draft 2019-09. + */ + public static final String V201909 = "https://json-schema.org/draft/2019-09/schema"; + + /** + * Draft 2020-12. + */ + public static final String V202012 = "https://json-schema.org/draft/2020-12/schema"; +} diff --git a/src/main/java/com/networknt/schema/SpecVersion.java b/src/main/java/com/networknt/schema/SpecVersion.java index 188e350ca..3f02cd91a 100644 --- a/src/main/java/com/networknt/schema/SpecVersion.java +++ b/src/main/java/com/networknt/schema/SpecVersion.java @@ -21,11 +21,11 @@ public class SpecVersion { public enum VersionFlag { - V4(1 << 0, "https://json-schema.org/draft-04/schema"), - V6(1 << 1, "https://json-schema.org/draft-06/schema"), - V7(1 << 2, "https://json-schema.org/draft-07/schema"), - V201909(1 << 3, "https://json-schema.org/draft/2019-09/schema"), - V202012(1 << 4, "https://json-schema.org/draft/2020-12/schema"); + V4(1 << 0, SchemaId.V4), + V6(1 << 1, SchemaId.V6), + V7(1 << 2, SchemaId.V7), + V201909(1 << 3, SchemaId.V201909), + V202012(1 << 4, SchemaId.V202012); private final long versionFlagValue; diff --git a/src/main/java/com/networknt/schema/Version201909.java b/src/main/java/com/networknt/schema/Version201909.java index 5114a806a..81db41595 100644 --- a/src/main/java/com/networknt/schema/Version201909.java +++ b/src/main/java/com/networknt/schema/Version201909.java @@ -5,7 +5,7 @@ import java.util.Map; public class Version201909 extends JsonSchemaVersion{ - private static final String URI = "https://json-schema.org/draft/2019-09/schema"; + private static final String URI = SchemaId.V201909; private static final String ID = "$id"; private static final Map VOCABULARY; diff --git a/src/main/java/com/networknt/schema/Version202012.java b/src/main/java/com/networknt/schema/Version202012.java index 9816bb9a2..581278369 100644 --- a/src/main/java/com/networknt/schema/Version202012.java +++ b/src/main/java/com/networknt/schema/Version202012.java @@ -5,7 +5,7 @@ import java.util.Map; public class Version202012 extends JsonSchemaVersion { - private static final String URI = "https://json-schema.org/draft/2020-12/schema"; + private static final String URI = SchemaId.V202012; private static final String ID = "$id"; private static final Map VOCABULARY; diff --git a/src/main/java/com/networknt/schema/Version4.java b/src/main/java/com/networknt/schema/Version4.java index 248dad904..8dd86aa79 100644 --- a/src/main/java/com/networknt/schema/Version4.java +++ b/src/main/java/com/networknt/schema/Version4.java @@ -3,8 +3,8 @@ import java.util.Arrays; public class Version4 extends JsonSchemaVersion{ - String URI = "https://json-schema.org/draft-04/schema"; - String ID = "id"; + private static final String URI = SchemaId.V4; + private static final String ID = "id"; static { // add version specific formats here. diff --git a/src/main/java/com/networknt/schema/Version6.java b/src/main/java/com/networknt/schema/Version6.java index 4459bb889..920b9520f 100644 --- a/src/main/java/com/networknt/schema/Version6.java +++ b/src/main/java/com/networknt/schema/Version6.java @@ -3,7 +3,7 @@ import java.util.Arrays; public class Version6 extends JsonSchemaVersion{ - private static final String URI = "https://json-schema.org/draft-06/schema"; + private static final String URI = SchemaId.V6; // Draft 6 uses "$id" private static final String ID = "$id"; diff --git a/src/main/java/com/networknt/schema/Version7.java b/src/main/java/com/networknt/schema/Version7.java index f5407d68b..ebf01d849 100644 --- a/src/main/java/com/networknt/schema/Version7.java +++ b/src/main/java/com/networknt/schema/Version7.java @@ -3,7 +3,7 @@ import java.util.Arrays; public class Version7 extends JsonSchemaVersion{ - private static final String URI = "https://json-schema.org/draft-07/schema"; + private static final String URI = SchemaId.V7; private static final String ID = "$id"; static { diff --git a/src/test/java/com/networknt/schema/DependentRequiredTest.java b/src/test/java/com/networknt/schema/DependentRequiredTest.java index 4ea15013d..2f4bf519d 100644 --- a/src/test/java/com/networknt/schema/DependentRequiredTest.java +++ b/src/test/java/com/networknt/schema/DependentRequiredTest.java @@ -16,7 +16,7 @@ class DependentRequiredTest { public static final String SCHEMA = "{ " + - " \"$schema\":\"http://json-schema.org/draft/2019-09/schema\"," + + " \"$schema\":\"https://json-schema.org/draft/2019-09/schema\"," + " \"type\": \"object\"," + " \"properties\": {" + " \"optional\": \"string\"," + diff --git a/src/test/java/com/networknt/schema/ExampleTest.java b/src/test/java/com/networknt/schema/ExampleTest.java index 6b9f2229f..018d70a67 100644 --- a/src/test/java/com/networknt/schema/ExampleTest.java +++ b/src/test/java/com/networknt/schema/ExampleTest.java @@ -41,12 +41,12 @@ public void example() throws Exception { + " }\r\n" + "}"; // The example-main.json schema defines $schema with Draft 07 - assertEquals("https://json-schema.org/draft-07/schema", schema.getValidationContext().getMetaSchema().getUri()); + assertEquals(SchemaId.V7, schema.getValidationContext().getMetaSchema().getUri()); Set errors = schema.validate(JsonMapperFactory.getInstance().readTree(input)); assertEquals(1, errors.size()); // The example-ref.json schema defines $schema with Draft 2019-09 JsonSchema refSchema = schema.getValidationContext().getSchemaResources().get("https://www.example.org/example-ref.json#"); - assertEquals("https://json-schema.org/draft/2019-09/schema", refSchema.getValidationContext().getMetaSchema().getUri()); + assertEquals(SchemaId.V201909, refSchema.getValidationContext().getMetaSchema().getUri()); } } diff --git a/src/test/java/com/networknt/schema/Issue314Test.java b/src/test/java/com/networknt/schema/Issue314Test.java index 1ac0200ee..725b985af 100644 --- a/src/test/java/com/networknt/schema/Issue314Test.java +++ b/src/test/java/com/networknt/schema/Issue314Test.java @@ -9,7 +9,7 @@ public class Issue314Test { JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)) .addMetaSchema( JsonMetaSchema.builder( - "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0", + "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#", JsonMetaSchema.getV7()) .build()) .build(); diff --git a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java index 27e464fe0..15bef4f4b 100644 --- a/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java +++ b/src/test/java/com/networknt/schema/JsonSchemaFactoryUriCacheTest.java @@ -33,11 +33,11 @@ private void runCacheTest(boolean enableCache) throws JsonProcessingException { CustomURIFetcher fetcher = new CustomURIFetcher(); JsonSchemaFactory factory = buildJsonSchemaFactory(fetcher, enableCache); SchemaLocation schemaUri = SchemaLocation.of("cache:uri_mapping/schema1.json"); - String schema = "{ \"$schema\": \"https://json-schema.org/draft/2020-12/schema#\", \"title\": \"json-object-with-schema\", \"type\": \"string\" }"; + String schema = "{ \"$schema\": \"https://json-schema.org/draft/2020-12/schema\", \"title\": \"json-object-with-schema\", \"type\": \"string\" }"; fetcher.addResource(schemaUri.getAbsoluteIri(), schema); assertEquals(objectMapper.readTree(schema), factory.getSchema(schemaUri, new SchemaValidatorsConfig()).schemaNode); - String modifiedSchema = "{ \"$schema\": \"https://json-schema.org/draft/2020-12/schema#\", \"title\": \"json-object-with-schema\", \"type\": \"object\" }"; + String modifiedSchema = "{ \"$schema\": \"https://json-schema.org/draft/2020-12/schema\", \"title\": \"json-object-with-schema\", \"type\": \"object\" }"; fetcher.addResource(schemaUri.getAbsoluteIri(), modifiedSchema); assertEquals(objectMapper.readTree(enableCache ? schema : modifiedSchema), factory.getSchema(schemaUri, new SchemaValidatorsConfig()).schemaNode); diff --git a/src/test/java/com/networknt/schema/RefTest.java b/src/test/java/com/networknt/schema/RefTest.java index c628b3597..068491277 100644 --- a/src/test/java/com/networknt/schema/RefTest.java +++ b/src/test/java/com/networknt/schema/RefTest.java @@ -27,7 +27,7 @@ void shouldLoadRelativeClasspathReference() throws JsonMappingException, JsonPro + " }\r\n" + " }\r\n" + "}"; - assertEquals("https://json-schema.org/draft-04/schema", schema.getValidationContext().getMetaSchema().getUri()); + assertEquals(SchemaId.V4, schema.getValidationContext().getMetaSchema().getUri()); Set errors = schema.validate(OBJECT_MAPPER.readTree(input)); assertEquals(1, errors.size()); ValidationMessage error = errors.iterator().next(); @@ -51,7 +51,7 @@ void shouldLoadSchemaResource() throws JsonMappingException, JsonProcessingExcep + " }\r\n" + " }\r\n" + "}"; - assertEquals("https://json-schema.org/draft-04/schema", schema.getValidationContext().getMetaSchema().getUri()); + assertEquals(SchemaId.V4, schema.getValidationContext().getMetaSchema().getUri()); Set errors = schema.validate(OBJECT_MAPPER.readTree(input)); assertEquals(1, errors.size()); ValidationMessage error = errors.iterator().next(); @@ -62,8 +62,8 @@ void shouldLoadSchemaResource() throws JsonMappingException, JsonProcessingExcep assertEquals("field1", error.getProperty()); JsonSchema driver = schema.getValidationContext().getSchemaResources().get("https://www.example.org/driver#"); JsonSchema common = schema.getValidationContext().getSchemaResources().get("https://www.example.org/common#"); - assertEquals("https://json-schema.org/draft-04/schema", driver.getValidationContext().getMetaSchema().getUri()); - assertEquals("https://json-schema.org/draft-07/schema", common.getValidationContext().getMetaSchema().getUri()); + assertEquals(SchemaId.V4, driver.getValidationContext().getMetaSchema().getUri()); + assertEquals(SchemaId.V7, common.getValidationContext().getMetaSchema().getUri()); } } diff --git a/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java b/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java index 4b7e74265..06c35e4bb 100644 --- a/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java +++ b/src/test/java/com/networknt/schema/UnknownMetaSchemaTest.java @@ -64,7 +64,7 @@ public void testNormalize() throws JsonSchemaException { String uri02 = "http://json-schema.org/draft-07/schema#"; String uri03 = "http://json-schema.org/draft-07/schema?key=value"; String uri04 = "http://json-schema.org/draft-07/schema?key=value&key2=value2"; - String expected = "https://json-schema.org/draft-07/schema"; + String expected = SchemaId.V7; Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri01)); Assertions.assertEquals(expected, JsonSchemaFactory.normalizeMetaSchemaUri(uri02)); diff --git a/src/test/resources/draft2019-09/issue375.json b/src/test/resources/draft2019-09/issue375.json index fe6206fd2..66979c0dd 100644 --- a/src/test/resources/draft2019-09/issue375.json +++ b/src/test/resources/draft2019-09/issue375.json @@ -1,5 +1,5 @@ { - "$schema": "https://json-schema.org/draft/2019-09/schema#", + "$schema": "https://json-schema.org/draft/2019-09/schema", "title": "test", "description": "asdasdint", "type": "object", diff --git a/src/test/resources/schema/example-main.json b/src/test/resources/schema/example-main.json index e7fa885b2..05a84361f 100644 --- a/src/test/resources/schema/example-main.json +++ b/src/test/resources/schema/example-main.json @@ -1,5 +1,5 @@ { - "$schema": "https://json-schema.org/draft-07/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required" : ["DriverProperties"], "properties": { diff --git a/src/test/resources/schema/issue575-2019-09.json b/src/test/resources/schema/issue575-2019-09.json index 55187f54e..7d984cfbd 100644 --- a/src/test/resources/schema/issue575-2019-09.json +++ b/src/test/resources/schema/issue575-2019-09.json @@ -1,5 +1,5 @@ { - "$schema" : "https://json-schema.org/draft/2019-09/schema#", + "$schema" : "https://json-schema.org/draft/2019-09/schema", "title": "Test Time Zone Schema (for testing time zones with negative offsets)", "type": "object", "properties": { From 66a7be7bbaa8b52549433ca3e776e655b37ba321 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 09:41:27 +0800 Subject: [PATCH 47/65] Refactor --- src/main/java/com/networknt/schema/JsonSchemaFactory.java | 1 - .../java/com/networknt/schema/resource/UriSchemaLoader.java | 2 +- src/test/java/com/networknt/schema/UriMappingTest.java | 4 ++-- .../draft4/extra/uri_mapping/invalid-schema-uri.json | 5 +++++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index c1a78d269..8d38b659f 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -27,7 +27,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; -import java.net.URISyntaxException; import java.util.Collection; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/com/networknt/schema/resource/UriSchemaLoader.java b/src/main/java/com/networknt/schema/resource/UriSchemaLoader.java index 7a7aaeb9c..671c08630 100644 --- a/src/main/java/com/networknt/schema/resource/UriSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/UriSchemaLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 the original author or authors. + * Copyright (c) 2016 Network New Technologies Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/networknt/schema/UriMappingTest.java b/src/test/java/com/networknt/schema/UriMappingTest.java index c0566344e..19934f42b 100644 --- a/src/test/java/com/networknt/schema/UriMappingTest.java +++ b/src/test/java/com/networknt/schema/UriMappingTest.java @@ -68,7 +68,7 @@ public void testBuilderUriMappingUri() throws IOException { @Test public void testBuilderExampleMappings() throws IOException { JsonSchemaFactory instance = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - SchemaLocation example = SchemaLocation.of("http://example.com/invalid/schema/url"); + SchemaLocation example = SchemaLocation.of("https://example.com/invalid/schema/url"); // first test that attempting to use example URL throws an error try { JsonSchema schema = instance.getSchema(example); @@ -124,7 +124,7 @@ public void testValidatorConfigExampleMappings() throws IOException { JsonSchemaFactory instance = JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)).build(); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - SchemaLocation example = SchemaLocation.of("http://example.com/invalid/schema/url"); + SchemaLocation example = SchemaLocation.of("https://example.com/invalid/schema/url"); // first test that attempting to use example URL throws an error try { JsonSchema schema = instance.getSchema(example, config); diff --git a/src/test/resources/draft4/extra/uri_mapping/invalid-schema-uri.json b/src/test/resources/draft4/extra/uri_mapping/invalid-schema-uri.json index 34e8dfefd..59ab16133 100644 --- a/src/test/resources/draft4/extra/uri_mapping/invalid-schema-uri.json +++ b/src/test/resources/draft4/extra/uri_mapping/invalid-schema-uri.json @@ -6,5 +6,10 @@ { "publicURL": "http://example.com/invalid/schema/url", "localURL": "resource:/draft4/extra/uri_mapping/example-schema.json" + }, + { + "publicURL": "https://example.com/invalid/schema/url", + "localURL": "resource:/draft4/extra/uri_mapping/example-schema.json" } + ] From ee251a430a6b8855c3979745be95a2a602e5bc62 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 10:20:47 +0800 Subject: [PATCH 48/65] update comment --- src/main/java/com/networknt/schema/JsonSchema.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index d08ce4b84..7580013de 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -257,8 +257,8 @@ public JsonSchema getSubSchema(JsonNodePath fragment) { parent); parent = subSchema; } else { - // In earlier drafts this can be because the parent is incorrect draft4\extra\classpath\schema.json - // This follows the old logic for handleNullNode + // In Draft 4-7 the $id indicates a base uri change and not a schema resource + // See test for draft4\extra\classpath\schema.json JsonSchema found = parent.findSchemaResourceRoot().fetchSubSchemaNode(this.validationContext); if (found != null) { found = found.getSubSchema(fragment); From 8e4c76010909a56222fae0003364fb8538bb2926 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:11:05 +0800 Subject: [PATCH 49/65] Fix dynamic ref circular dependency --- .../networknt/schema/DynamicRefValidator.java | 18 +++++++++- .../java/com/networknt/schema/JsonSchema.java | 36 ++++++++++++++++--- .../networknt/schema/JsonSchemaFactory.java | 24 ++++++++++++- .../com/networknt/schema/RefValidator.java | 13 +++---- 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/networknt/schema/DynamicRefValidator.java b/src/main/java/com/networknt/schema/DynamicRefValidator.java index f1f0fd742..d2ce7a67a 100644 --- a/src/main/java/com/networknt/schema/DynamicRefValidator.java +++ b/src/main/java/com/networknt/schema/DynamicRefValidator.java @@ -161,6 +161,22 @@ public void preloadJsonSchema() { } catch (RuntimeException e) { throw new JsonSchemaException(e); } - jsonSchema.initializeValidators(); + // Check for circular dependency + // Only one cycle is pre-loaded + // The rest of the cycles will load at execution time depending on the input + // data + SchemaLocation schemaLocation = jsonSchema.getSchemaLocation(); + JsonSchema check = jsonSchema; + boolean circularDependency = false; + while(check.getEvaluationParentSchema() != null) { + check = check.getEvaluationParentSchema(); + if (check.getSchemaLocation().equals(schemaLocation)) { + circularDependency = true; + break; + } + } + if(!circularDependency) { + jsonSchema.initializeValidators(); + } } } diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 7580013de..a7989a59c 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -235,11 +235,25 @@ public JsonSchema getRefSchema(JsonNodePath fragment) { } } + /** + * Gets the sub schema given the json pointer fragment. + * + * @param fragment the json pointer fragment + * @return the schema + */ public JsonSchema getSubSchema(JsonNodePath fragment) { JsonSchema document = findSchemaResourceRoot(); JsonSchema parent = document; JsonSchema subSchema = null; for (int x = 0; x < fragment.getNameCount(); x++) { + /* + * The sub schema is created by iterating through the parents in order to + * maintain the lexical parent schema context. + * + * If this is created directly from the schema node pointed to by the json + * pointer, the lexical context is lost and this will affect $ref resolution due + * to $id changes in the lexical scope. + */ Object segment = fragment.getElement(x); JsonNode subSchemaNode = parent.getNode(segment); if (subSchemaNode != null) { @@ -253,13 +267,22 @@ public JsonSchema getSubSchema(JsonNodePath fragment) { schemaLocation = schemaLocation.append(segment.toString()); evaluationPath = evaluationPath.append(segment.toString()); } + /* + * The parent validation context is used to create as there can be changes in + * $schema is later drafts which means the validation context can change. + */ subSchema = parent.getValidationContext().newSchema(schemaLocation, evaluationPath, subSchemaNode, parent); parent = subSchema; } else { - // In Draft 4-7 the $id indicates a base uri change and not a schema resource - // See test for draft4\extra\classpath\schema.json - JsonSchema found = parent.findSchemaResourceRoot().fetchSubSchemaNode(this.validationContext); + /* + * This means that the fragment wasn't found in the document. + * + * In Draft 4-7 the $id indicates a base uri change and not a schema resource so this might not be the right document. + * + * See test for draft4\extra\classpath\schema.json + */ + JsonSchema found = document.findSchemaResourceRoot().fetchSubSchemaNode(this.validationContext); if (found != null) { found = found.getSubSchema(fragment); } @@ -834,10 +857,15 @@ public List getValidators() { */ public void initializeValidators() { if (!this.validatorsLoaded) { - this.validatorsLoaded = true; for (final JsonValidator validator : getValidators()) { validator.preloadJsonSchema(); } + /* + * This is only set to true after the preload as it may throw an exception for + * instance if the remote host is unavailable and we may want to be able to try + * again. + */ + this.validatorsLoaded = true; } } diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 8d38b659f..ea7403f98 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -212,10 +212,32 @@ public static Builder builder(final JsonSchemaFactory blueprint) { return builder; } + /** + * Creates a json schema from initial input. + * + * @param schemaUri the schema location + * @param schemaNode the schema data node + * @param config the config to use + * @return the schema + */ protected JsonSchema newJsonSchema(final SchemaLocation schemaUri, final JsonNode schemaNode, final SchemaValidatorsConfig config) { final ValidationContext validationContext = createValidationContext(schemaNode, config); - return doCreate(validationContext, getSchemaLocation(schemaUri, schemaNode, validationContext), + JsonSchema jsonSchema = doCreate(validationContext, getSchemaLocation(schemaUri, schemaNode, validationContext), new JsonNodePath(validationContext.getConfig().getPathType()), schemaNode, null, false); + try { + /* + * Attempt to preload and resolve $refs for performance. + */ + jsonSchema.initializeValidators(); + } catch (Exception e) { + /* + * Do nothing here to allow the schema to be cached even if the remote $ref + * cannot be resolved at this time. If the developer wants to ensure that all + * remote $refs are currently resolvable they need to call initializeValidators + * themselves. + */ + } + return jsonSchema; } public JsonSchema create(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema) { diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 42f93a8eb..e8c5016e1 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -144,15 +144,12 @@ private static JsonSchema getJsonSchema(JsonSchema parent, String refValue, String refValueOriginal, JsonNodePath evaluationPath) { - JsonNode node = parent.getRefSchemaNode(refValue); + // This should be processing json pointer fragments only JsonNodePath fragment = SchemaLocation.Fragment.of(refValue); - if (node != null) { - String schemaReference = resolve(parent, refValueOriginal); - return validationContext.getSchemaReferences().computeIfAbsent(schemaReference, key -> { - return parent.getSubSchema(fragment); - }); - } - return null; + String schemaReference = resolve(parent, refValueOriginal); + return validationContext.getSchemaReferences().computeIfAbsent(schemaReference, key -> { + return parent.getSubSchema(fragment); + }); } @Override From 675b915773dbdd8c788a3f20e5d35e81b431edee Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:03:11 +0800 Subject: [PATCH 50/65] Update docs and readme --- README.md | 86 +++++++++++++++++++++++++++++------------- doc/upgrading.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 doc/upgrading.md diff --git a/README.md b/README.md index eb78978a9..5bf8afddc 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,19 @@ [![Javadocs](http://www.javadoc.io/badge/com.networknt/json-schema-validator.svg)](https://www.javadoc.io/doc/com.networknt/json-schema-validator) -This is a Java implementation of the [JSON Schema Core Draft v4, v6, v7, v2019-09 and v2020-12](http://json-schema.org/latest/json-schema-core.html) specification for JSON schema validation. Information on the compatibility support for each version can be found [here](doc/compatibility.md). +This is a Java implementation of the [JSON Schema Core Draft v4, v6, v7, v2019-09 and v2020-12](http://json-schema.org/latest/json-schema-core.html) specification for JSON schema validation. -In addition, it also works for OpenAPI 3.0 request/response validation with some [configuration flags](doc/config.md). For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The default JSON parser is the [Jackson](https://github.com/FasterXML/jackson) that is the most popular one. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design. +In addition, it also works for OpenAPI 3.0 request/response validation with some [configuration flags](doc/config.md). For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The default JSON parser is the [Jackson](https://github.com/FasterXML/jackson) that is the most popular one. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design. + +## JSON Schema Draft Specification Compatibility + +Information on the compatibility support for each version, including known issues, can be found in the [Compatibility with JSON Schema versions](doc/compatibility.md) document. + +## Upgrading to new versions + +Information on notable or breaking changes when upgrading the library can be found in the [Upgrading to new versions](doc/upgrading.md) document. This library can contain breaking changes in minor version releases. + +For the latest version, please check the [Releases](https://github.com/networknt/json-schema-validator/releases) page. ## Why this library @@ -60,8 +70,6 @@ Here are the dependencies: ``` -**Note**: Up to version [1.0.81](https://github.com/networknt/json-schema-validator/blob/1.0.81/pom.xml#L99), the dependency `org.apache.commons:commons-lang3` was included as a runtime dependency. Starting with [1.0.82](https://github.com/networknt/json-schema-validator/releases/tag/1.0.82) it is not required anymore. - #### Community This library is very active with a lot of contributors. New features and bug fixes are handled quickly by the team members. Because it is an essential dependency of the [light-4j](https://github.com/networknt/light-4j) framework in the same GitHub organization, it will be evolved and maintained along with the framework. @@ -70,37 +78,70 @@ This library is very active with a lot of contributors. New features and bug fix The library supports Java 8 and up. If you want to build from the source code, you need to install JDK 8 locally. To support multiple version of JDK, you can use [SDKMAN](https://www.networknt.com/tool/sdk/) -## Dependency +## Usage + +### Adding the dependency This package is available on Maven central. -Maven: +#### Maven: ```xml com.networknt json-schema-validator - 1.0.87 - - - - - org.apache.commons - commons-lang3 - - + 1.2.0 ``` -Gradle: +#### Gradle: ```java dependencies { - implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.0.87'); + implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.2.0'); } ``` -For the latest version, please check the [release](https://github.com/networknt/json-schema-validator/releases) page. +### Validating inputs against a schema + +The following example demonstrates how inputs is validated against a schema. It comprises the following steps. + +* Creating a schema factory with the default schema dialect and how the schemas can be retrieved. + * Configuring mapping the `$id` to a retrieval URI using `schemaMappers`. + * Configuring how the schemas are loaded using the retrieval URI using `schemaLoaders`. + For instance a `Map schemas` containing a mapping of retrieval URI to schema data as a `String` can by configured using `builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(schemas))`. This also accepts a `Function schemaRetrievalFunction`. +* Creating a configuration for controlling validator behavior. +* Loading a schema from a schema location along with the validator configuration. +* Using the schema to validate the data along with setting any execution specific configuration like for instance the locale or whether format assertions are enabled. + +```java +// This creates a schema factory that will use Draft 2012-12 as the default if $schema is not specified in the schema data. If $schema is specified in the schema data then that schema dialect will be used instead and this version is ignored. +JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> { + // This creates a mapping from $id which starts with https://www.example.org/ to the retrieval URI classpath:schema/ + builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:schema/")); +}); + +SchemaValidatorsConfig config = new SchemaValidatorsConfig(); +// By default JSON Path is used for reporting the instance path and evaluation path +config.setPathType(PathType.JSON_POINTER); +// By default the JDK regular expression implementation which is not ECMA 262 compliant is used +config.setEcma262Validator(true); + +// Due to the mapping the schema will be retrieved from the classpath at classpath:schema/example-main.json. If the schema data does not specify an $id the absolute IRI of the schema location will be used as the $id. +JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of("https://www.example.org/example-main.json"), config); +String input = "{\r\n" + + " \"DriverProperties\": {\r\n" + + " \"CommonProperties\": {\r\n" + + " \"field2\": \"abc-def-xyz\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + +Set assertions = schema.validate(JsonMapperFactory.getInstance().readTree(input), executionContext -> { + // By default since Draft 2019-09 the format keyword only generates annotations and not assertions + executionContext.getConfig().setFormatAssertionsEnabled(true); +}); +``` ## [Quick Start](doc/quickstart.md) @@ -130,15 +171,6 @@ For the latest version, please check the [release](https://github.com/networknt/ ## [Validating RFC 3339 durations](doc/duration.md) - -## Known issues - -I have just updated the test suites from the [official website](https://github.com/json-schema-org/JSON-Schema-Test-Suite) as the old ones were copied from another Java validator. Now there are several issues that need to be addressed. All of them are edge cases, in my opinion, but need to be investigated. As my old test suites were inherited from another Java JSON Schema Validator, I guess other Java Validator would have the same issues as these issues are in the Java language itself. - -[#7](https://github.com/networknt/json-schema-validator/issues/7) - -[#5](https://github.com/networknt/json-schema-validator/issues/5) - ## Projects The [light-rest-4j](https://github.com/networknt/light-rest-4j), [light-graphql-4j](https://github.com/networknt/light-graphql-4j) and [light-hybrid-4j](https://github.com/networknt/light-hybrid-4j) use this library to validate the request and response based on the specifications. If you are using other frameworks like Spring Boot, you can use the [OpenApiValidator](https://github.com/mservicetech/openapi-schema-validation), a generic OpenAPI 3.0 validator based on the OpenAPI 3.0 specification. diff --git a/doc/upgrading.md b/doc/upgrading.md new file mode 100644 index 000000000..0eab6becf --- /dev/null +++ b/doc/upgrading.md @@ -0,0 +1,98 @@ +## Upgrading to new versions + +This contains information on the notable or breaking changes in each version. + +### 1.3.0 + +This adds support for Draft 2020-12 + +This adds support for the following keywords +* `$dynamicRef` +* `$dynamicAnchor` +* `$vocabulary` + +This refactors the schema retrieval codes as the ID is based on IRI and not URI. + +Note that Java does not support IRIs. See https://cr.openjdk.org/%7Edfuchs/writeups/updating-uri/ for details. + +The following are removed and replaced by `SchemaLoader` and `SchemaMapper`. +* `URIFactory` - No replacement. The resolve logic is in `AbsoluteIRI`. +* `URISchemeFactory` - No replacement as `URIFactory` isn't required anymore. +* `URISchemeFetcher` - No replacement. The `SchemaLoaders` are iterated and called. +* `URITranslator` - Replaced by `SchemaMapper`. +* `URLFactory` - No replacement as `URIFactory` isn't required anymore. +* `URLFetcher` - Replaced by `UriSchemaLoader`. +* `URNURIFactory` - No replacement as `URIFactory` isn't required anymore. + +The `SchemaLoader` and `SchemaMapper` are configured in the `JsonSchemaFactory.Builder`. + +As per the specification. The `format` keyword since Draft 2019-09 no longer generates assertions by default. + +This can be changed by using a custom meta schema with the relevant `$vocabulary` or by setting the execution configuration to enable format assertions. + +### 1.2.0 + +The following are a summary of the changes +* Paths are now specified using the `JsonNodePath`. The paths are `instanceLocation`, `schemaLocation` and `evaluationPath`. The meaning of these paths are as defined in the [specification](https://github.com/json-schema-org/json-schema-spec/blob/main/jsonschema-validation-output-machines.md). +* Schema Location comprises an absolute IRI component and a fragment that is a `JsonNodePath` that is typically a JSON pointer +* Rename `at` to `instanceLocation`. Note that for the `required` validator the error message `instanceLocation` does not point to the missing property to be consistent with the [specification](https://json-schema.org/draft/2020-12/json-schema-core#section-12.4.2). The `ValidationMessage` now contains a `property` attribute if this is required. +* Rename `schemaPath` to `schemaLocation`. This should generally be an absolute IRI with a fragment particularly in later drafts. +* Add `evaluationPath` + +`JsonValidator` +* Now contains `getSchemaLocation` and `getEvaluationPath` in the interface +* Implementations now need a constructor that takes in `schemaLocation` and `evaluationPath` +* The `validate` method uses `JsonNodePath` for the `instanceLocation` +* The `validate` method with just the `rootNode` has been removed + +`JsonSchemaWalker` +* The `walk` method uses `JsonNodePath` for the `instanceLocation` + +`WalkEvent` +* Rename `at` to `instanceLocation` +* Rename `schemaPath` to `schemaLocation` +* Add `evaluationPath` +* Rename `keyWordName` to `keyword` + +`WalkListenerRunner` +* Rename `at` to `instanceLocation` +* Rename `schemaPath` to `schemaLocation` +* Add `evaluationPath` + +`BaseJsonValidator` +* The `atPath` methods are removed. Use `JsonNodePath.append` to get the path of the child +* The `buildValidationMessage` methods are removed. Use the `message` builder method instead. + +`CollectorContext` +* The `evaluatedProperties` and `evaluatedItems` are now `Collection` + +`JsonSchema` +* The validator keys are now using `evaluationPath` instead of `schemaPath` +* The `@deprecated` constructor methods have been removed + +`ValidatorTypeCode` +* The `customMessage` has been removed. This made the `ValidatorTypeCode` mutable if the feature was used as the enum is a shared instance. The logic for determining the `customMessage` has been moved to the validator. +* The creation of `newValidator` instances now uses a functional interface instead of reflection. + +`ValidatorState` +* The `ValidatorState` is now a property of the `ExecutionContext`. This change is largely to improve performance. The `CollectorContext.get` method is particularly slow for this use case. + +### 1.1.0 + +Removes use of `ThreadLocal` to store context and explicitly passes the context as a parameter where needed. + +The following are the main API changes, typically to accept an `ExecutionContext` as a parameter + +* `com.networknt.schema.JsonSchema` +* `com.networknt.schema.JsonValidator` +* `com.networknt.schema.Format` +* `com.networknt.schema.walk.JsonSchemaWalker` +* `com.networknt.schema.walk.WalkEvent` + +`JsonSchema` was modified to optionally accept an `ExecutionContext` for the `validate`, `validateAndCollect` and `walk` methods. For methods where no `ExecutionContext` is supplied, one is created for each run in the `createExecutionContext` method in `JsonSchema`. + +`ValidationResult` was modified to store the `ExecutionContext` of the run which is also a means of reusing the context, by passing this context information from the `ValidationResult` to following runs. + +### 1.0.82 + +Up to version [1.0.81](https://github.com/networknt/json-schema-validator/blob/1.0.81/pom.xml#L99), the dependency `org.apache.commons:commons-lang3` was included as a runtime dependency. Starting with [1.0.82](https://github.com/networknt/json-schema-validator/releases/tag/1.0.82) it is not required anymore. \ No newline at end of file From ac9a5282a108c5b161ff892f2546f22fb40a0812 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:14:28 +0800 Subject: [PATCH 51/65] Update example --- .../com/networknt/schema/ExampleTest.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/networknt/schema/ExampleTest.java b/src/test/java/com/networknt/schema/ExampleTest.java index 018d70a67..d4441ace1 100644 --- a/src/test/java/com/networknt/schema/ExampleTest.java +++ b/src/test/java/com/networknt/schema/ExampleTest.java @@ -25,7 +25,7 @@ public class ExampleTest { @Test - public void example() throws Exception { + public void exampleSchemaLocation() throws Exception { // This creates a schema factory that will use Draft 2012-12 as the default if $schema is not specified in the initial schema JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> { builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:schema/")); @@ -49,4 +49,28 @@ public void example() throws Exception { JsonSchema refSchema = schema.getValidationContext().getSchemaResources().get("https://www.example.org/example-ref.json#"); assertEquals(SchemaId.V201909, refSchema.getValidationContext().getMetaSchema().getUri()); } + + @Test + public void exampleClasspath() throws Exception { + // This creates a schema factory that will use Draft 2012-12 as the default if $schema is not specified in the initial schema + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of("classpath:schema/example-main.json"), config); + String input = "{\r\n" + + " \"DriverProperties\": {\r\n" + + " \"CommonProperties\": {\r\n" + + " \"field2\": \"abc-def-xyz\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + // The example-main.json schema defines $schema with Draft 07 + assertEquals(SchemaId.V7, schema.getValidationContext().getMetaSchema().getUri()); + Set errors = schema.validate(JsonMapperFactory.getInstance().readTree(input)); + assertEquals(1, errors.size()); + + // The example-ref.json schema defines $schema with Draft 2019-09 + JsonSchema refSchema = schema.getValidationContext().getSchemaResources().get("classpath:schema/example-ref.json#"); + assertEquals(SchemaId.V201909, refSchema.getValidationContext().getMetaSchema().getUri()); + } } From cc90ae126dbe4971eb2db148271c7d110d888a1f Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:15:24 +0800 Subject: [PATCH 52/65] Fix 877 --- .../java/com/networknt/schema/JsonSchema.java | 58 +++++++++++-------- .../com/networknt/schema/Issue877Test.java | 42 ++++++++++++++ 2 files changed, 76 insertions(+), 24 deletions(-) create mode 100644 src/test/java/com/networknt/schema/Issue877Test.java diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index a7989a59c..6e347efb1 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -764,41 +764,51 @@ private ValidationResult walkAtNodeInternal(ExecutionContext executionContext, J } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { - Set validationMessages = new LinkedHashSet<>(); + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema) { + Set errors = new LinkedHashSet<>(); + CollectorContext collectorContext = executionContext.getCollectorContext(); // Walk through all the JSONWalker's. - getValidators().forEach(jsonWalker -> { - JsonNodePath evaluationPathWithKeyword = jsonWalker.getEvaluationPath(); + for (JsonValidator v : getValidators()) { + JsonNodePath evaluationPathWithKeyword = v.getEvaluationPath(); try { // Call all the pre-walk listeners. If at least one of the pre walk listeners // returns SKIP, then skip the walk. if (this.keywordWalkListenerRunner.runPreWalkListeners(executionContext, - evaluationPathWithKeyword.getName(-1), - node, - rootNode, - instanceLocation, - jsonWalker.getEvaluationPath(), - jsonWalker.getSchemaLocation(), - this.schemaNode, + evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, + v.getEvaluationPath(), v.getSchemaLocation(), this.schemaNode, this.parentSchema, this.validationContext, this.validationContext.getJsonSchemaFactory())) { - validationMessages.addAll(jsonWalker.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema)); + Set results = null; + Scope parentScope = collectorContext.enterDynamicScope(this); + try { + results = v.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + } finally { + Scope scope = collectorContext.exitDynamicScope(); + if (results == null || 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()); + } + } + } } } finally { // Call all the post-walk listeners. this.keywordWalkListenerRunner.runPostWalkListeners(executionContext, - evaluationPathWithKeyword.getName(-1), - node, - rootNode, - instanceLocation, - jsonWalker.getEvaluationPath(), - jsonWalker.getSchemaLocation(), - this.schemaNode, - this.parentSchema, - this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages); + evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, + v.getEvaluationPath(), v.getSchemaLocation(), this.schemaNode, + this.parentSchema, this.validationContext, this.validationContext.getJsonSchemaFactory(), + errors); } - }); - - return validationMessages; + } + return errors; } /************************ END OF WALK METHODS **********************************/ diff --git a/src/test/java/com/networknt/schema/Issue877Test.java b/src/test/java/com/networknt/schema/Issue877Test.java new file mode 100644 index 000000000..b2ca05357 --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue877Test.java @@ -0,0 +1,42 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue877Test { + @Test + public void test() throws Exception { + String schemaData = "{\n" + + " \"type\": \"object\",\n" + + " \"unevaluatedProperties\": false\n" + + "}"; + + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + JsonSchema schema = jsonSchemaFactory.getSchema(schemaData); + String input = "{}"; + ValidationResult result = schema.walk(JsonMapperFactory.getInstance().readTree(input), true); + assertEquals(0, result.getValidationMessages().size()); + + input = ""; + result = schema.walk(JsonMapperFactory.getInstance().readTree(input), true); + assertEquals(1, result.getValidationMessages().size()); + } +} From 48f57721276d74e135760ba2b2100725ce67cf98 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:29:19 +0800 Subject: [PATCH 53/65] Fix rebase --- src/test/java/com/networknt/schema/Issue928Test.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/networknt/schema/Issue928Test.java b/src/test/java/com/networknt/schema/Issue928Test.java index 6e23d8822..df933da48 100644 --- a/src/test/java/com/networknt/schema/Issue928Test.java +++ b/src/test/java/com/networknt/schema/Issue928Test.java @@ -10,8 +10,8 @@ public class Issue928Test { private JsonSchemaFactory factoryFor(SpecVersion.VersionFlag version) { return JsonSchemaFactory .builder(JsonSchemaFactory.getInstance(version)) - .objectMapper(mapper) - .schemaLoaderBuilder(builder -> builder.mapPrefix("https://example.org", "classpath:")) + .jsonMapper(mapper) + .schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://example.org", "classpath:")) .build(); } From c732b05e2d9ee3ef1515bf8db26a056bb629f7df Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 25 Jan 2024 20:47:45 +0800 Subject: [PATCH 54/65] Add tests --- .../com/networknt/schema/Issue475Test.java | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/test/java/com/networknt/schema/Issue475Test.java diff --git a/src/test/java/com/networknt/schema/Issue475Test.java b/src/test/java/com/networknt/schema/Issue475Test.java new file mode 100644 index 000000000..4aaa5e01c --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue475Test.java @@ -0,0 +1,124 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +/** + * Tests for validation of schema against meta schema. + */ +public class Issue475Test { + private static final String VALID_INPUT = "{ \n" + + " \"type\": \"object\", \n" + + " \"properties\": { \n" + + " \"key\": { \n" + + " \"title\" : \"My key\", \n" + + " \"type\": \"array\" \n" + + " } \n" + + " }\n" + + "}"; + + private static final String INVALID_INPUT = "{ \n" + + " \"type\": \"object\", \n" + + " \"properties\": { \n" + + " \"key\": { \n" + + " \"title\" : \"My key\", \n" + + " \"type\": \"blabla\" \n" + + " } \n" + + " }\n" + + "}"; + + @Test + public void draft4() throws Exception { + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V4, builder -> builder + .schemaMappers(schemaMappers -> schemaMappers.mapPrefix("http://json-schema.org", "classpath:"))); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(SchemaId.V4), config); + + Set assertions = schema.validate(JsonMapperFactory.getInstance().readTree(INVALID_INPUT)); + assertEquals(2, assertions.size()); + + assertions = schema.validate(JsonMapperFactory.getInstance().readTree(VALID_INPUT)); + assertEquals(0, assertions.size()); + } + + @Test + public void draft6() throws Exception { + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V6, builder -> builder + .schemaMappers(schemaMappers -> schemaMappers.mapPrefix("http://json-schema.org", "classpath:"))); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(SchemaId.V6), config); + + Set assertions = schema.validate(JsonMapperFactory.getInstance().readTree(INVALID_INPUT)); + assertEquals(2, assertions.size()); + + assertions = schema.validate(JsonMapperFactory.getInstance().readTree(VALID_INPUT)); + assertEquals(0, assertions.size()); + } + + @Test + public void draft7() throws Exception { + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V7, builder -> builder + .schemaMappers(schemaMappers -> schemaMappers.mapPrefix("http://json-schema.org", "classpath:"))); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(SchemaId.V7), config); + + Set assertions = schema.validate(JsonMapperFactory.getInstance().readTree(INVALID_INPUT)); + assertEquals(2, assertions.size()); + + assertions = schema.validate(JsonMapperFactory.getInstance().readTree(VALID_INPUT)); + assertEquals(0, assertions.size()); + } + + @Test + public void draft201909() throws Exception { + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V201909, builder -> builder + .schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://json-schema.org", "classpath:"))); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(SchemaId.V201909), config); + + Set assertions = schema.validate(JsonMapperFactory.getInstance().readTree(INVALID_INPUT)); + assertEquals(2, assertions.size()); + + assertions = schema.validate(JsonMapperFactory.getInstance().readTree(VALID_INPUT)); + assertEquals(0, assertions.size()); + } + + @Test + public void draft202012() throws Exception { + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder + .schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://json-schema.org", "classpath:"))); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(SchemaId.V202012), config); + + Set assertions = schema.validate(JsonMapperFactory.getInstance().readTree(INVALID_INPUT)); + assertEquals(2, assertions.size()); + + assertions = schema.validate(JsonMapperFactory.getInstance().readTree(VALID_INPUT)); + assertEquals(0, assertions.size()); + } +} From 20b7ccb5316dcd14e78c32675849c6f37cac6d94 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 26 Jan 2024 08:29:57 +0800 Subject: [PATCH 55/65] Shift package --- .../com/networknt/schema/ContentMediaTypeValidator.java | 1 + src/main/java/com/networknt/schema/JsonSchemaFactory.java | 2 ++ .../schema/{ => serialization}/JsonMapperFactory.java | 7 ++++++- .../schema/{ => serialization}/YamlMapperFactory.java | 7 ++++++- .../java/com/networknt/schema/AbstractJsonSchemaTest.java | 1 + .../com/networknt/schema/AbstractJsonSchemaTestSuite.java | 1 + .../com/networknt/schema/BaseJsonSchemaValidatorTest.java | 1 + src/test/java/com/networknt/schema/ExampleTest.java | 1 + src/test/java/com/networknt/schema/Issue475Test.java | 1 + src/test/java/com/networknt/schema/Issue877Test.java | 1 + src/test/java/com/networknt/schema/suite/TestSource.java | 2 +- 11 files changed, 22 insertions(+), 3 deletions(-) rename src/main/java/com/networknt/schema/{ => serialization}/JsonMapperFactory.java (84%) rename src/main/java/com/networknt/schema/{ => serialization}/YamlMapperFactory.java (84%) diff --git a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java index 36e67bd8e..38bf8780b 100644 --- a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java +++ b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.serialization.JsonMapperFactory; /** * Validation for contentMediaType keyword. diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index ea7403f98..88f9508a4 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.resource.*; +import com.networknt.schema.serialization.JsonMapperFactory; +import com.networknt.schema.serialization.YamlMapperFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/com/networknt/schema/JsonMapperFactory.java b/src/main/java/com/networknt/schema/serialization/JsonMapperFactory.java similarity index 84% rename from src/main/java/com/networknt/schema/JsonMapperFactory.java rename to src/main/java/com/networknt/schema/serialization/JsonMapperFactory.java index d6edf05b2..7a24e2939 100644 --- a/src/main/java/com/networknt/schema/JsonMapperFactory.java +++ b/src/main/java/com/networknt/schema/serialization/JsonMapperFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.networknt.schema; +package com.networknt.schema.serialization; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; @@ -30,6 +30,11 @@ private static class Holder { private static final ObjectMapper INSTANCE = JsonMapper.builder().build(); } + /** + * Gets the singleton instance of the JsonMapper. + * + * @return the JsonMapper + */ public static ObjectMapper getInstance() { return Holder.INSTANCE; } diff --git a/src/main/java/com/networknt/schema/YamlMapperFactory.java b/src/main/java/com/networknt/schema/serialization/YamlMapperFactory.java similarity index 84% rename from src/main/java/com/networknt/schema/YamlMapperFactory.java rename to src/main/java/com/networknt/schema/serialization/YamlMapperFactory.java index 9a9741e98..ea0b7a2df 100644 --- a/src/main/java/com/networknt/schema/YamlMapperFactory.java +++ b/src/main/java/com/networknt/schema/serialization/YamlMapperFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.networknt.schema; +package com.networknt.schema.serialization; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; @@ -30,6 +30,11 @@ private static class Holder { private static final ObjectMapper INSTANCE = YAMLMapper.builder().build(); } + /** + * Gets the singleton instance of the YAMLMapper. + * + * @return the YAMLMapper + */ public static ObjectMapper getInstance() { return Holder.INSTANCE; } diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java index 71a8a7556..7df7b310b 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.serialization.JsonMapperFactory; import java.io.IOException; import java.io.InputStream; diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java index b2adaba44..66a6856a4 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.serialization.JsonMapperFactory; import com.networknt.schema.suite.TestCase; import com.networknt.schema.suite.TestSource; import com.networknt.schema.suite.TestSpec; diff --git a/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java b/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java index d5444b260..c001c628d 100644 --- a/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java +++ b/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.serialization.JsonMapperFactory; import java.io.IOException; import java.io.InputStream; diff --git a/src/test/java/com/networknt/schema/ExampleTest.java b/src/test/java/com/networknt/schema/ExampleTest.java index d4441ace1..ae0a6bdb7 100644 --- a/src/test/java/com/networknt/schema/ExampleTest.java +++ b/src/test/java/com/networknt/schema/ExampleTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.serialization.JsonMapperFactory; public class ExampleTest { @Test diff --git a/src/test/java/com/networknt/schema/Issue475Test.java b/src/test/java/com/networknt/schema/Issue475Test.java index 4aaa5e01c..e9944cbd6 100644 --- a/src/test/java/com/networknt/schema/Issue475Test.java +++ b/src/test/java/com/networknt/schema/Issue475Test.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.serialization.JsonMapperFactory; /** * Tests for validation of schema against meta schema. diff --git a/src/test/java/com/networknt/schema/Issue877Test.java b/src/test/java/com/networknt/schema/Issue877Test.java index b2ca05357..ab5740457 100644 --- a/src/test/java/com/networknt/schema/Issue877Test.java +++ b/src/test/java/com/networknt/schema/Issue877Test.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.serialization.JsonMapperFactory; public class Issue877Test { @Test diff --git a/src/test/java/com/networknt/schema/suite/TestSource.java b/src/test/java/com/networknt/schema/suite/TestSource.java index 1d606d884..5479edc70 100644 --- a/src/test/java/com/networknt/schema/suite/TestSource.java +++ b/src/test/java/com/networknt/schema/suite/TestSource.java @@ -12,7 +12,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.MismatchedInputException; -import com.networknt.schema.JsonMapperFactory; +import com.networknt.schema.serialization.JsonMapperFactory; public class TestSource { protected static final TypeReference> testCaseType = new TypeReference>() { /* intentionally empty */}; From 8f0e250b18993b3bafc2f60abda4c3c4774e7b05 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 26 Jan 2024 09:09:17 +0800 Subject: [PATCH 56/65] Add convenience methods for validation --- .../com/networknt/schema/InputFormat.java | 31 +++ .../java/com/networknt/schema/JsonSchema.java | 219 ++++++++++++++++-- 2 files changed, 235 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/networknt/schema/InputFormat.java diff --git a/src/main/java/com/networknt/schema/InputFormat.java b/src/main/java/com/networknt/schema/InputFormat.java new file mode 100644 index 000000000..f375dce49 --- /dev/null +++ b/src/main/java/com/networknt/schema/InputFormat.java @@ -0,0 +1,31 @@ +/* + * 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; + +/** + * The input data format. + */ +public enum InputFormat { + /** + * JSON. + */ + JSON, + + /** + * YAML. + */ + YAML +} diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 6e347efb1..435b07027 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -16,10 +16,13 @@ package com.networknt.schema; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.CollectorContext.Scope; import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.serialization.JsonMapperFactory; +import com.networknt.schema.serialization.YamlMapperFactory; import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner; import com.networknt.schema.walk.WalkListenerRunner; @@ -59,7 +62,7 @@ static JsonSchema from(ValidationContext validationContext, SchemaLocation schem private boolean hasNoFragment(SchemaLocation schemaLocation) { return this.schemaLocation.getFragment() == null || this.schemaLocation.getFragment().getNameCount() == 0; } - + private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { super(schemaLocation.resolve(validationContext.resolveSchemaId(schemaNode)), evaluationPath, schemaNode, parent, @@ -296,7 +299,7 @@ public JsonSchema getSubSchema(JsonNodePath fragment) { } return subSchema; } - + protected JsonNode getNode(Object propertyOrIndex) { JsonNode node = getSchemaNode(); JsonNode value = null; @@ -630,7 +633,7 @@ public Set validate(JsonNode rootNode, Consumer the result type - * @param rootNode the root note + * @param rootNode the root node * @param format the formatter * @return the result */ @@ -680,6 +683,147 @@ public T validate(JsonNode rootNode, OutputFormat format, Consumer + * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. + * + * @param input the input + * @param inputFormat the inputFormat + * @return A list of ValidationMessage if there is any validation error, or an + * empty list if there is no error. + */ + public Set validate(String input, InputFormat inputFormat) { + return validate(deserialize(input, inputFormat), OutputFormat.DEFAULT); + } + + /** + * Validate the given input string using the input format, starting at the root + * of the data path. + *

+ * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. + * + * @param input the input + * @param inputFormat the inputFormat + * @param executionCustomizer the execution customizer + * @return the assertions + */ + public Set validate(String input, InputFormat inputFormat, ExecutionContextCustomizer executionCustomizer) { + return validate(deserialize(input, inputFormat), OutputFormat.DEFAULT, executionCustomizer); + } + + /** + * Validate the given input string using the input format, starting at the root + * of the data path. + *

+ * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. + * + * @param input the input + * @param inputFormat the inputFormat + * @param executionCustomizer the execution customizer + * @return the assertions + */ + public Set validate(String input, InputFormat inputFormat, Consumer executionCustomizer) { + return validate(deserialize(input, inputFormat), OutputFormat.DEFAULT, executionCustomizer); + } + + /** + * Validates the given input string using the input format, starting at the root + * of the data path. The output will be formatted using the formatter specified. + *

+ * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. + * + * @param the result type + * @param input the input + * @param inputFormat the inputFormat + * @param format the formatter + * @return the result + */ + public T validate(String input, InputFormat inputFormat, OutputFormat format) { + return validate(deserialize(input, inputFormat), format, (ExecutionContextCustomizer) null); + } + + /** + * Validates the given input string using the input format, starting at the root + * of the data path. The output will be formatted using the formatter specified. + *

+ * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. + * + * @param the result type + * @param input the input + * @param inputFormat the inputFormat + * @param format the formatter + * @param executionCustomizer the execution customizer + * @return the result + */ + public T validate(String input, InputFormat inputFormat, OutputFormat format, ExecutionContextCustomizer executionCustomizer) { + return validate(createExecutionContext(), deserialize(input, inputFormat), format, executionCustomizer); + } + + /** + * Validates the given input string using the input format, starting at the root + * of the data path. The output will be formatted using the formatter specified. + *

+ * Note that since Draft 2019-09 by default format generates only annotations + * and not assertions. + *

+ * Use {@link ExecutionConfig#setFormatAssertionsEnabled(Boolean)} to override + * the default. + * + * @param the result type + * @param input the input + * @param inputFormat the inputFormat + * @param format the formatter + * @param executionCustomizer the execution customizer + * @return the result + */ + public T validate(String input, InputFormat inputFormat, OutputFormat format, Consumer executionCustomizer) { + return validate(createExecutionContext(), deserialize(input, inputFormat), format, (executionContext, validationContext) -> { + executionCustomizer.accept(executionContext); + }); + } + + /** + * Deserialize string to JsonNode. + * + * @param input the input + * @param inputFormat the format + * @return the JsonNode. + */ + private JsonNode deserialize(String input, InputFormat inputFormat) { + try { + if (InputFormat.JSON.equals(inputFormat)) { + return JsonMapperFactory.getInstance().readTree(input); + } else if (InputFormat.YAML.equals(inputFormat)) { + return YamlMapperFactory.getInstance().readTree(input); + } + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid input", e); + } + throw new IllegalArgumentException("Unsupported input format "+inputFormat); + } + public ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode node) { return validateAndCollect(executionContext, node, node, atRoot()); } @@ -723,24 +867,69 @@ public ValidationResult validateAndCollect(JsonNode node) { /*********************** START OF WALK METHODS **********************************/ /** - * Walk the JSON node - * @param executionContext ExecutionContext - * @param node JsonNode - * @param shouldValidateSchema indicator on validation + * Walk the JSON node. + * + * @param executionContext the execution context + * @param node the input + * @param validate true to validate the input against the schema * - * @return result of ValidationResult + * @return the validation result */ - public ValidationResult walk(ExecutionContext executionContext, JsonNode node, boolean shouldValidateSchema) { - return walkAtNodeInternal(executionContext, node, node, atRoot(), shouldValidateSchema); + public ValidationResult walk(ExecutionContext executionContext, JsonNode node, boolean validate) { + return walkAtNodeInternal(executionContext, node, node, atRoot(), validate); } - - public ValidationResult walk(JsonNode node, boolean shouldValidateSchema) { - return walk(createExecutionContext(), node, shouldValidateSchema); + + /** + * Walk the input. + * + * @param executionContext the execution context + * @param input the input + * @param inputFormat the input format + * @param validate true to validate the input against the schema + * @return the validation result + */ + public ValidationResult walk(ExecutionContext executionContext, String input, InputFormat inputFormat, + boolean validate) { + JsonNode node = deserialize(input, inputFormat); + return walkAtNodeInternal(executionContext, node, node, atRoot(), validate); } + /** + * Walk the JSON node. + * + * @param node the input + * @param validate true to validate the input against the schema + * @return the validation result + */ + public ValidationResult walk(JsonNode node, boolean validate) { + return walk(createExecutionContext(), node, validate); + } + + /** + * Walk the input. + * + * @param input the input + * @param inputFormat the input format + * @param validate true to validate the input against the schema + * @return the validation result + */ + public ValidationResult walk(String input, InputFormat inputFormat, boolean validate) { + return walk(createExecutionContext(), deserialize(input, inputFormat), validate); + } + + /** + * Walk at the node. + * + * @param executionContext the execution content + * @param node the current node + * @param rootNode the root node + * @param instanceLocation the instance location + * @param validate true to validate the input against the schema + * @return the validation result + */ public ValidationResult walkAtNode(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation, boolean shouldValidateSchema) { - return walkAtNodeInternal(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + JsonNodePath instanceLocation, boolean validate) { + return walkAtNodeInternal(executionContext, node, rootNode, instanceLocation, validate); } private ValidationResult walkAtNodeInternal(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, From 8693babbd0362b78a9d892918ecc81771ad1ec68 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 26 Jan 2024 09:27:31 +0800 Subject: [PATCH 57/65] Refactor --- .../networknt/schema/JsonSchemaFactory.java | 83 +++++++++++++++---- .../com/networknt/schema/ExampleTest.java | 17 ++-- 2 files changed, 74 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 88f9508a4..f68d1d4e5 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -31,6 +31,7 @@ import java.net.URI; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; @@ -47,8 +48,8 @@ public static class Builder { private ObjectMapper yamlMapper = null; private String defaultMetaSchemaURI; private final ConcurrentMap jsonMetaSchemas = new ConcurrentHashMap(); - private SchemaLoaders.Builder schemaLoadersBuilder = SchemaLoaders.builder(); - private SchemaMappers.Builder schemaMappersBuilder = SchemaMappers.builder(); + private SchemaLoaders.Builder schemaLoadersBuilder = null; + private SchemaMappers.Builder schemaMappersBuilder = null; private boolean enableUriSchemaCache = true; public Builder jsonMapper(final ObjectMapper jsonMapper) { @@ -84,11 +85,17 @@ public Builder enableUriSchemaCache(boolean enableUriSchemaCache) { } public Builder schemaLoaders(Consumer schemaLoadersBuilderCustomizer) { + if (this.schemaLoadersBuilder == null) { + this.schemaLoadersBuilder = SchemaLoaders.builder(); + } schemaLoadersBuilderCustomizer.accept(this.schemaLoadersBuilder); return this; } public Builder schemaMappers(Consumer schemaMappersBuilderCustomizer) { + if (this.schemaMappersBuilder == null) { + this.schemaMappersBuilder = SchemaMappers.builder(); + } schemaMappersBuilderCustomizer.accept(this.schemaMappersBuilder); return this; } @@ -116,7 +123,9 @@ public JsonSchemaFactory build() { private final Map jsonMetaSchemas; private final ConcurrentMap uriSchemaCache = new ConcurrentHashMap<>(); private final boolean enableUriSchemaCache; - + + private static final List DEFAULT_SCHEMA_LOADERS = SchemaLoaders.builder().build(); + private static final List DEFAULT_SCHEMA_MAPPERS = SchemaMappers.builder().build(); private JsonSchemaFactory( final ObjectMapper jsonMapper, @@ -128,10 +137,6 @@ private JsonSchemaFactory( final boolean enableUriSchemaCache) { if (defaultMetaSchemaURI == null || defaultMetaSchemaURI.trim().isEmpty()) { throw new IllegalArgumentException("defaultMetaSchemaURI must not be null or empty"); - } else if (schemaLoadersBuilder == null) { - throw new IllegalArgumentException("SchemaLoaders must not be null"); - } else if (schemaMappersBuilder == null) { - throw new IllegalArgumentException("SchemaMappers must not be null"); } else if (jsonMetaSchemas == null || jsonMetaSchemas.isEmpty()) { throw new IllegalArgumentException("Json Meta Schemas must not be null or empty"); } else if (jsonMetaSchemas.get(normalizeMetaSchemaUri(defaultMetaSchemaURI)) == null) { @@ -142,7 +147,9 @@ private JsonSchemaFactory( this.defaultMetaSchemaURI = defaultMetaSchemaURI; this.schemaLoadersBuilder = schemaLoadersBuilder; this.schemaMappersBuilder = schemaMappersBuilder; - this.schemaLoader = new DefaultSchemaLoader(schemaLoadersBuilder.build(), schemaMappersBuilder.build()); + this.schemaLoader = new DefaultSchemaLoader( + schemaLoadersBuilder != null ? schemaLoadersBuilder.build() : DEFAULT_SCHEMA_LOADERS, + schemaMappersBuilder != null ? schemaMappersBuilder.build() : DEFAULT_SCHEMA_MAPPERS); this.jsonMetaSchemas = jsonMetaSchemas; this.enableUriSchemaCache = enableUriSchemaCache; } @@ -162,25 +169,45 @@ public static Builder builder() { return new Builder(); } + /** + * Creates a factory with a default schema dialect. The schema dialect will only + * be used if the input does not specify a $schema. + * + * @param versionFlag the default dialect + * @return the factory + */ public static JsonSchemaFactory getInstance(SpecVersion.VersionFlag versionFlag) { - JsonSchemaVersion jsonSchemaVersion = checkVersion(versionFlag); - JsonMetaSchema metaSchema = jsonSchemaVersion.getInstance(); - return builder() - .defaultMetaSchemaURI(metaSchema.getUri()) - .addMetaSchema(metaSchema) - .build(); + return getInstance(versionFlag, null); } + /** + * Creates a factory with a default schema dialect. The schema dialect will only + * be used if the input does not specify a $schema. + * + * @param versionFlag the default dialect + * @param customizer to customze the factory + * @return the factory + */ public static JsonSchemaFactory getInstance(SpecVersion.VersionFlag versionFlag, Consumer customizer) { JsonSchemaVersion jsonSchemaVersion = checkVersion(versionFlag); JsonMetaSchema metaSchema = jsonSchemaVersion.getInstance(); JsonSchemaFactory.Builder builder = builder().defaultMetaSchemaURI(metaSchema.getUri()) .addMetaSchema(metaSchema); - customizer.accept(builder); + if (customizer != null) { + customizer.accept(builder); + } return builder.build(); } + /** + * Gets the json schema version to get the meta schema. + *

+ * This throws an {@link IllegalArgumentException} for an unsupported value. + * + * @param versionFlag the schema dialect + * @return the version + */ public static JsonSchemaVersion checkVersion(SpecVersion.VersionFlag versionFlag){ if (null == versionFlag) return null; switch (versionFlag) { @@ -209,8 +236,12 @@ public static Builder builder(final JsonSchemaFactory blueprint) { .defaultMetaSchemaURI(blueprint.defaultMetaSchemaURI) .jsonMapper(blueprint.jsonMapper) .yamlMapper(blueprint.yamlMapper); - builder.schemaLoadersBuilder.with(blueprint.schemaLoadersBuilder); - builder.schemaMappersBuilder.with(blueprint.schemaMappersBuilder); + if (blueprint.schemaLoadersBuilder != null) { + builder.schemaLoadersBuilder = SchemaLoaders.builder().with(blueprint.schemaLoadersBuilder); + } + if (blueprint.schemaMappersBuilder != null) { + builder.schemaMappersBuilder = SchemaMappers.builder().with(blueprint.schemaMappersBuilder); + } return builder; } @@ -251,6 +282,19 @@ private JsonSchema doCreate(ValidationContext validationContext, SchemaLocation schemaNode, parentSchema, suppressSubSchemaRetrieval); } + /** + * Determines the validation context to use for the schema given the parent + * validation context. + *

+ * This is typically the same validation context unless the schema has a + * different $schema from the parent. + *

+ * If the schema does not define a $schema, the parent should be used. + * + * @param validationContext the parent validation context + * @param schemaNode the schema node + * @return the validation context to use + */ private ValidationContext withMetaSchema(ValidationContext validationContext, JsonNode schemaNode) { JsonMetaSchema metaSchema = getMetaSchema(schemaNode, validationContext.getConfig()); if (metaSchema != null && !metaSchema.getUri().equals(validationContext.getMetaSchema().getUri())) { @@ -422,6 +466,11 @@ protected ObjectMapper getJsonMapper() { return this.jsonMapper != null ? this.jsonMapper : JsonMapperFactory.getInstance(); } + /** + * Creates a schema validators config. + * + * @return the schema validators config + */ protected SchemaValidatorsConfig createSchemaValidatorsConfig() { return new SchemaValidatorsConfig(); } diff --git a/src/test/java/com/networknt/schema/ExampleTest.java b/src/test/java/com/networknt/schema/ExampleTest.java index ae0a6bdb7..81e6f18b7 100644 --- a/src/test/java/com/networknt/schema/ExampleTest.java +++ b/src/test/java/com/networknt/schema/ExampleTest.java @@ -22,15 +22,14 @@ import org.junit.jupiter.api.Test; import com.networknt.schema.SpecVersion.VersionFlag; -import com.networknt.schema.serialization.JsonMapperFactory; public class ExampleTest { @Test public void exampleSchemaLocation() throws Exception { // This creates a schema factory that will use Draft 2012-12 as the default if $schema is not specified in the initial schema - JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> { - builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:schema/")); - }); + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> + builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:schema/")) + ); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setPathType(PathType.JSON_POINTER); JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of("https://www.example.org/example-main.json"), config); @@ -43,14 +42,14 @@ public void exampleSchemaLocation() throws Exception { + "}"; // The example-main.json schema defines $schema with Draft 07 assertEquals(SchemaId.V7, schema.getValidationContext().getMetaSchema().getUri()); - Set errors = schema.validate(JsonMapperFactory.getInstance().readTree(input)); - assertEquals(1, errors.size()); + Set assertions = schema.validate(input, InputFormat.JSON); + assertEquals(1, assertions.size()); // The example-ref.json schema defines $schema with Draft 2019-09 JsonSchema refSchema = schema.getValidationContext().getSchemaResources().get("https://www.example.org/example-ref.json#"); assertEquals(SchemaId.V201909, refSchema.getValidationContext().getMetaSchema().getUri()); } - + @Test public void exampleClasspath() throws Exception { // This creates a schema factory that will use Draft 2012-12 as the default if $schema is not specified in the initial schema @@ -67,8 +66,8 @@ public void exampleClasspath() throws Exception { + "}"; // The example-main.json schema defines $schema with Draft 07 assertEquals(SchemaId.V7, schema.getValidationContext().getMetaSchema().getUri()); - Set errors = schema.validate(JsonMapperFactory.getInstance().readTree(input)); - assertEquals(1, errors.size()); + Set assertions = schema.validate(input, InputFormat.JSON); + assertEquals(1, assertions.size()); // The example-ref.json schema defines $schema with Draft 2019-09 JsonSchema refSchema = schema.getValidationContext().getSchemaResources().get("classpath:schema/example-ref.json#"); From 7f21fa2037b4fcec4110bbead5708a7ac9c19b7a Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:59:16 +0800 Subject: [PATCH 58/65] Update docs --- README.md | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5bf8afddc..1c2fdb2ce 100644 --- a/README.md +++ b/README.md @@ -58,16 +58,41 @@ Here are the dependencies: ```xml + + org.slf4j + slf4j-api + ${version.slf4j} + + + + com.fasterxml.jackson.core jackson-databind ${version.jackson} - org.slf4j - slf4j-api - ${version.slf4j} + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${version.jackson} + + + + + com.ethlo.time + itu + ${version.itu} + + + + org.jruby.joni + joni + ${version.joni} + true + + ``` #### Community @@ -116,10 +141,10 @@ The following example demonstrates how inputs is validated against a schema. It ```java // This creates a schema factory that will use Draft 2012-12 as the default if $schema is not specified in the schema data. If $schema is specified in the schema data then that schema dialect will be used instead and this version is ignored. -JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> { +JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> // This creates a mapping from $id which starts with https://www.example.org/ to the retrieval URI classpath:schema/ - builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:schema/")); -}); + builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:schema/")) +); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); // By default JSON Path is used for reporting the instance path and evaluation path @@ -137,12 +162,50 @@ String input = "{\r\n" + " }\r\n" + "}"; -Set assertions = schema.validate(JsonMapperFactory.getInstance().readTree(input), executionContext -> { +Set assertions = schema.validate(input, InputFormat.JSON, executionContext -> { // By default since Draft 2019-09 the format keyword only generates annotations and not assertions executionContext.getConfig().setFormatAssertionsEnabled(true); }); ``` +### Validating a schema against a meta schema + +The following example demonstrates how a schema is validated against a meta schema. + +This is actually the same as validating inputs against a schema except in this case the input is the schema and the schema used is the meta schema. + +```java +JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> + // This creates a mapping to load the meta schema from the library classpath instead of remotely + // This is better for performance and the remote may choose not to service the request + // For instance Cloudflare will block requests that have older Java User-Agent strings eg. Java/1. + builder.schemaMappers(schemaMappers -> + schemaMappers.mapPrefix("https://json-schema.org", "classpath:").mapPrefix("http://json-schema.org", "classpath:")) +); + +SchemaValidatorsConfig config = new SchemaValidatorsConfig(); +// By default JSON Path is used for reporting the instance path and evaluation path +config.setPathType(PathType.JSON_POINTER); +// By default the JDK regular expression implementation which is not ECMA 262 compliant is used +config.setEcma262Validator(true); + +// Due to the mapping the meta schema will be retrieved from the classpath at classpath:draft/2020-12/schema. +JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(SchemaId.V202012), config); +String input = "{ \n" + + " \"type\": \"object\", \n" + + " \"properties\": { \n" + + " \"key\": { \n" + + " \"title\" : \"My key\", \n" + + " \"type\": \"blabla\" \n" + + " } \n" + + " }\n" + + "}"; +Set assertions = schema.validate(input, InputFormat.JSON, executionContext -> { + // By default since Draft 2019-09 the format keyword only generates annotations and not assertions + executionContext.getConfig().setFormatAssertionsEnabled(true); +}); +``` + ## [Quick Start](doc/quickstart.md) ## [Validators](doc/validators.md) From 753ee00f87708b0eacf007508e1c34e0aff33227 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 26 Jan 2024 11:53:39 +0800 Subject: [PATCH 59/65] Update docs --- README.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1c2fdb2ce..01fb90ead 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ The OpenAPI 3.0 specification is using JSON schema to validate the request/respo Following the design principle of the Light Platform, this library has minimum dependencies to ensure there are no dependency conflicts when using it. -Here are the dependencies: +The following are the dependencies that will automatically be included when this library is included. ```xml @@ -84,7 +84,14 @@ Here are the dependencies: itu ${version.itu} +``` + +The following are the optional dependencies that may be required for certain options. + +These are not automatically included and setting the relevant option without adding the library will result in a `ClassNotFoundException`. +```xml + org.jruby.joni @@ -92,7 +99,21 @@ Here are the dependencies: ${version.joni} true +``` + +The YAML dependency can be excluded if this is not required. Attempting to process schemas or input that are YAML will result in a `ClassNotFoundException`. +```xml + + com.networknt + json-schema-validator + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + ``` #### Community @@ -150,7 +171,8 @@ SchemaValidatorsConfig config = new SchemaValidatorsConfig(); // By default JSON Path is used for reporting the instance path and evaluation path config.setPathType(PathType.JSON_POINTER); // By default the JDK regular expression implementation which is not ECMA 262 compliant is used -config.setEcma262Validator(true); +// Note that setting this to true requires including the optional joni dependency +// config.setEcma262Validator(true); // Due to the mapping the schema will be retrieved from the classpath at classpath:schema/example-main.json. If the schema data does not specify an $id the absolute IRI of the schema location will be used as the $id. JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of("https://www.example.org/example-main.json"), config); @@ -187,7 +209,8 @@ SchemaValidatorsConfig config = new SchemaValidatorsConfig(); // By default JSON Path is used for reporting the instance path and evaluation path config.setPathType(PathType.JSON_POINTER); // By default the JDK regular expression implementation which is not ECMA 262 compliant is used -config.setEcma262Validator(true); +// Note that setting this to true requires including the optional joni dependency +// config.setEcma262Validator(true); // Due to the mapping the meta schema will be retrieved from the classpath at classpath:draft/2020-12/schema. JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(SchemaId.V202012), config); From 13f8df6e11ab8415b83aff7189f489c32768ab80 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:32:58 +0800 Subject: [PATCH 60/65] Update docs --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 01fb90ead..621837844 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag. ); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -// By default JSON Path is used for reporting the instance path and evaluation path +// By default JSON Path is used for reporting the instance location and evaluation path config.setPathType(PathType.JSON_POINTER); // By default the JDK regular expression implementation which is not ECMA 262 compliant is used // Note that setting this to true requires including the optional joni dependency @@ -177,9 +177,9 @@ config.setPathType(PathType.JSON_POINTER); // Due to the mapping the schema will be retrieved from the classpath at classpath:schema/example-main.json. If the schema data does not specify an $id the absolute IRI of the schema location will be used as the $id. JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of("https://www.example.org/example-main.json"), config); String input = "{\r\n" - + " \"DriverProperties\": {\r\n" - + " \"CommonProperties\": {\r\n" - + " \"field2\": \"abc-def-xyz\"\r\n" + + " \"main\": {\r\n" + + " \"common\": {\r\n" + + " \"field\": \"invalidfield\"\r\n" + " }\r\n" + " }\r\n" + "}"; @@ -206,7 +206,7 @@ JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag. ); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -// By default JSON Path is used for reporting the instance path and evaluation path +// By default JSON Path is used for reporting the instance location and evaluation path config.setPathType(PathType.JSON_POINTER); // By default the JDK regular expression implementation which is not ECMA 262 compliant is used // Note that setting this to true requires including the optional joni dependency @@ -219,7 +219,7 @@ String input = "{ \n" + " \"properties\": { \n" + " \"key\": { \n" + " \"title\" : \"My key\", \n" - + " \"type\": \"blabla\" \n" + + " \"type\": \"invalidtype\" \n" + " } \n" + " }\n" + "}"; From 0e37dffe0724cdea8eed2c7c292a927b640cc458 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 26 Jan 2024 14:41:01 +0800 Subject: [PATCH 61/65] Refactor schema loaders --- .../resource/ClasspathSchemaLoader.java | 13 ++++++++---- .../schema/resource/DefaultSchemaLoader.java | 16 +++++++++++++++ .../schema/resource/SchemaLoaders.java | 20 +------------------ 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java b/src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java index 366c15e3d..1807f1edd 100644 --- a/src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/ClasspathSchemaLoader.java @@ -27,14 +27,19 @@ public class ClasspathSchemaLoader implements SchemaLoader { @Override public InputStreamSource getSchema(AbsoluteIri absoluteIri) { - String scheme = absoluteIri.getScheme(); - if (scheme.startsWith("classpath") || scheme.startsWith("resource")) { + String iri = absoluteIri != null ? absoluteIri.toString() : ""; + String name = null; + if (iri.startsWith("classpath:")) { + name = iri.substring(10); + } else if (iri.startsWith("resource:")) { + name = iri.substring(9); + } + if (name != null) { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if (classLoader == null) { classLoader = SchemaLoader.class.getClassLoader(); } ClassLoader loader = classLoader; - String name = absoluteIri.toString().substring(scheme.length() + 1); if (name.startsWith("//")) { name = name.substring(2); } @@ -45,7 +50,7 @@ public InputStreamSource getSchema(AbsoluteIri absoluteIri) { result = loader.getResourceAsStream(resource.substring(1)); } if (result == null) { - throw new FileNotFoundException(absoluteIri.toString()); + throw new FileNotFoundException(iri); } return result; }; diff --git a/src/main/java/com/networknt/schema/resource/DefaultSchemaLoader.java b/src/main/java/com/networknt/schema/resource/DefaultSchemaLoader.java index e31b7930d..f84251ec5 100644 --- a/src/main/java/com/networknt/schema/resource/DefaultSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/DefaultSchemaLoader.java @@ -15,6 +15,7 @@ */ package com.networknt.schema.resource; +import java.util.ArrayList; import java.util.List; import com.networknt.schema.AbsoluteIri; @@ -23,6 +24,15 @@ * Default {@link SchemaLoader}. */ public class DefaultSchemaLoader implements SchemaLoader { + private static final List DEFAULT; + + static { + List result = new ArrayList<>(); + result.add(new ClasspathSchemaLoader()); + result.add(new UriSchemaLoader()); + DEFAULT = result; + } + private final List schemaLoaders; private final List schemaMappers; @@ -46,6 +56,12 @@ public InputStreamSource getSchema(AbsoluteIri absoluteIri) { return result; } } + for (SchemaLoader loader : DEFAULT) { + InputStreamSource result = loader.getSchema(mappedResult); + if (result != null) { + return result; + } + } return null; } diff --git a/src/main/java/com/networknt/schema/resource/SchemaLoaders.java b/src/main/java/com/networknt/schema/resource/SchemaLoaders.java index b5b9288f7..607c4250c 100644 --- a/src/main/java/com/networknt/schema/resource/SchemaLoaders.java +++ b/src/main/java/com/networknt/schema/resource/SchemaLoaders.java @@ -28,17 +28,6 @@ public class SchemaLoaders extends ArrayList { private static final long serialVersionUID = 1L; - private static final ClasspathSchemaLoader CLASSPATH_SCHEMA_LOADER = new ClasspathSchemaLoader(); - private static final UriSchemaLoader URI_SCHEMA_LOADER = new UriSchemaLoader(); - private static final SchemaLoaders DEFAULT; - - static { - SchemaLoaders schemaLoaders = new SchemaLoaders(); - schemaLoaders.add(CLASSPATH_SCHEMA_LOADER); - schemaLoaders.add(URI_SCHEMA_LOADER); - DEFAULT = schemaLoaders; - } - public SchemaLoaders() { super(); } @@ -122,14 +111,7 @@ public Builder schemas(Function schemas) { * @return the schema loaders */ public SchemaLoaders build() { - if (this.values.isEmpty()) { - return DEFAULT; - } - SchemaLoaders result = new SchemaLoaders(); - result.add(CLASSPATH_SCHEMA_LOADER); - result.addAll(this.values); - result.add(URI_SCHEMA_LOADER); - return result; + return values; } } } From 8831ab4e26bce906ffd09840e01227d631fade68 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 26 Jan 2024 15:57:01 +0800 Subject: [PATCH 62/65] Update format routines and tests and fixes --- .../validator/routines/DomainValidator.java | 961 +++++++++--------- .../validator/routines/EmailValidator.java | 65 +- .../routines/InetAddressValidator.java | 88 +- .../com/networknt/schema/JsonMetaSchema.java | 4 +- .../networknt/schema/format/IriFormat.java | 15 +- .../com/networknt/schema/utils/RFC5892.java | 6 +- .../optional/format/hostname.json | 25 + .../optional/format/idn-email.json | 2 +- .../optional/format/idn-hostname.json | 35 +- .../optional/format/hostname.json | 25 + .../optional/format/idn-email.json | 2 +- .../optional/format/idn-hostname.json | 35 +- .../draft4/optional/format/hostname.json | 20 + .../tests/draft4/optional/format/ipv4.json | 5 + .../draft6/optional/format/hostname.json | 20 + .../tests/draft6/optional/format/ipv4.json | 5 + .../draft7/optional/format/hostname.json | 20 + .../draft7/optional/format/idn-email.json | 2 +- .../draft7/optional/format/idn-hostname.json | 30 +- .../tests/draft7/optional/format/iri.json | 4 +- 20 files changed, 785 insertions(+), 584 deletions(-) diff --git a/src/main/java/com/networknt/org/apache/commons/validator/routines/DomainValidator.java b/src/main/java/com/networknt/org/apache/commons/validator/routines/DomainValidator.java index 1ef74aaaf..c621e2877 100644 --- a/src/main/java/com/networknt/org/apache/commons/validator/routines/DomainValidator.java +++ b/src/main/java/com/networknt/org/apache/commons/validator/routines/DomainValidator.java @@ -63,351 +63,115 @@ */ public class DomainValidator implements Serializable { - /** Maximum allowable length ({@value}) of a domain name */ - private static final int MAX_DOMAIN_LENGTH = 253; - - private static final String[] EMPTY_STRING_ARRAY = {}; - - private static final long serialVersionUID = -4407125112880174009L; - - // Regular expression strings for hostnames (derived from RFC2396 and RFC 1123) - - // RFC2396: domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum - // Max 63 characters - private static final String DOMAIN_LABEL_REGEX = "\\p{Alnum}(?>[\\p{Alnum}-]{0,61}\\p{Alnum})?"; - - // RFC2396 toplabel = alpha | alpha *( alphanum | "-" ) alphanum - // Max 63 characters - private static final String TOP_LABEL_REGEX = "\\p{Alpha}(?>[\\p{Alnum}-]{0,61}\\p{Alnum})?"; - - // RFC2396 hostname = *( domainlabel "." ) toplabel [ "." ] - // Note that the regex currently requires both a domain label and a top level label, whereas - // the RFC does not. This is because the regex is used to detect if a TLD is present. - // If the match fails, input is checked against DOMAIN_LABEL_REGEX (hostnameRegex) - // RFC1123 sec 2.1 allows hostnames to start with a digit - private static final String DOMAIN_NAME_REGEX = - "^(?:" + DOMAIN_LABEL_REGEX + "\\.)+" + "(" + TOP_LABEL_REGEX + ")\\.?$"; - - private static final String UNEXPECTED_ENUM_VALUE = "Unexpected enum value: "; - - private final boolean allowLocal; - - private static class LazyHolder { // IODH - + /** + * enum used by {@link DomainValidator#updateTLDOverride(ArrayType, String[])} + * to determine which override array to update / fetch + * @since 1.5.0 + * @since 1.5.1 made public and added read-only array references + */ + public enum ArrayType { + /** Update (or get a copy of) the GENERIC_TLDS_PLUS table containing additonal generic TLDs */ + GENERIC_PLUS, + /** Update (or get a copy of) the GENERIC_TLDS_MINUS table containing deleted generic TLDs */ + GENERIC_MINUS, + /** Update (or get a copy of) the COUNTRY_CODE_TLDS_PLUS table containing additonal country code TLDs */ + COUNTRY_CODE_PLUS, + /** Update (or get a copy of) the COUNTRY_CODE_TLDS_MINUS table containing deleted country code TLDs */ + COUNTRY_CODE_MINUS, + /** Gets a copy of the generic TLDS table */ + GENERIC_RO, + /** Gets a copy of the country code table */ + COUNTRY_CODE_RO, + /** Gets a copy of the infrastructure table */ + INFRASTRUCTURE_RO, + /** Gets a copy of the local table */ + LOCAL_RO, /** - * Singleton instance of this validator, which - * doesn't consider local addresses as valid. + * Update (or get a copy of) the LOCAL_TLDS_PLUS table containing additional local TLDs + * @since 1.7 */ - private static final DomainValidator DOMAIN_VALIDATOR = new DomainValidator(false); - + LOCAL_PLUS, /** - * Singleton instance of this validator, which does - * consider local addresses valid. + * Update (or get a copy of) the LOCAL_TLDS_MINUS table containing deleted local TLDs + * @since 1.7 */ - private static final DomainValidator DOMAIN_VALIDATOR_WITH_LOCAL = new DomainValidator(true); - - } - - /** - * The above instances must only be returned via the getInstance() methods. - * This is to ensure that the override data arrays are properly protected. - */ - - /** - * RegexValidator for matching domains. - */ - private final RegexValidator domainRegex = - new RegexValidator(DOMAIN_NAME_REGEX); - /** - * RegexValidator for matching a local hostname - */ - // RFC1123 sec 2.1 allows hostnames to start with a digit - private final RegexValidator hostnameRegex = - new RegexValidator(DOMAIN_LABEL_REGEX); - - /** - * Returns the singleton instance of this validator. It - * will not consider local addresses as valid. - * @return the singleton instance of this validator - */ - public static synchronized DomainValidator getInstance() { - inUse = true; - return LazyHolder.DOMAIN_VALIDATOR; + LOCAL_MINUS + ; } - /** - * Returns the singleton instance of this validator, - * with local validation as required. - * @param allowLocal Should local addresses be considered valid? - * @return the singleton instance of this validator - */ - public static synchronized DomainValidator getInstance(final boolean allowLocal) { - inUse = true; - if(allowLocal) { - return LazyHolder.DOMAIN_VALIDATOR_WITH_LOCAL; + private static class IDNBUGHOLDER { + private static final boolean IDN_TOASCII_PRESERVES_TRAILING_DOTS = keepsTrailingDot(); + private static boolean keepsTrailingDot() { + final String input = "a."; // must be a valid name + return input.equals(IDN.toASCII(input)); } - return LazyHolder.DOMAIN_VALIDATOR; } /** - * Returns a new instance of this validator. - * The user can provide a list of {@link Item} entries which can - * be used to override the generic and country code lists. - * Note that any such entries override values provided by the - * {@link #updateTLDOverride(ArrayType, String[])} method - * If an entry for a particular type is not provided, then - * the class override (if any) is retained. - * - * @param allowLocal Should local addresses be considered valid? - * @param items - array of {@link Item} entries - * @return an instance of this validator + * Used to specify overrides when creating a new class. * @since 1.7 */ - public static synchronized DomainValidator getInstance(final boolean allowLocal, final List items) { - inUse = true; - return new DomainValidator(allowLocal, items); - } - - // instance variables allowing local overrides - final String[] myCountryCodeTLDsMinus; - final String[] myCountryCodeTLDsPlus; - final String[] myGenericTLDsPlus; - final String[] myGenericTLDsMinus; - final String[] myLocalTLDsPlus; - final String[] myLocalTLDsMinus; - /* - * N.B. It is vital that instances are immutable. - * This is because the default instances are shared. - */ - - // N.B. The constructors are deliberately private to avoid possible problems with unsafe publication. - // It is vital that the static override arrays are not mutable once they have been used in an instance - // The arrays could be copied into the instance variables, however if the static array were changed it could - // result in different settings for the shared default instances - - /** - * Private constructor. - */ - private DomainValidator(final boolean allowLocal) { - this.allowLocal = allowLocal; - // link to class overrides - myCountryCodeTLDsMinus = countryCodeTLDsMinus; - myCountryCodeTLDsPlus = countryCodeTLDsPlus; - myGenericTLDsPlus = genericTLDsPlus; - myGenericTLDsMinus = genericTLDsMinus; - myLocalTLDsPlus = localTLDsPlus; - myLocalTLDsMinus = localTLDsMinus; - } - - /** - * Private constructor, allowing local overrides - * @since 1.7 - */ - private DomainValidator(final boolean allowLocal, final List items) { - this.allowLocal = allowLocal; - - // default to class overrides - String[] ccMinus = countryCodeTLDsMinus; - String[] ccPlus = countryCodeTLDsPlus; - String[] genMinus = genericTLDsMinus; - String[] genPlus = genericTLDsPlus; - String[] localMinus = localTLDsMinus; - String[] localPlus = localTLDsPlus; - - // apply the instance overrides - for(final Item item: items) { - final String [] copy = new String[item.values.length]; - // Comparisons are always done with lower-case entries - for (int i = 0; i < item.values.length; i++) { - copy[i] = item.values[i].toLowerCase(Locale.ENGLISH); - } - Arrays.sort(copy); - switch(item.type) { - case COUNTRY_CODE_MINUS: { - ccMinus = copy; - break; - } - case COUNTRY_CODE_PLUS: { - ccPlus = copy; - break; - } - case GENERIC_MINUS: { - genMinus = copy; - break; - } - case GENERIC_PLUS: { - genPlus = copy; - break; - } - case LOCAL_MINUS: { - localMinus = copy; - break; - } - case LOCAL_PLUS: { - localPlus = copy; - break; - } - default: - break; - } - } - - // init the instance overrides - myCountryCodeTLDsMinus = ccMinus; - myCountryCodeTLDsPlus = ccPlus; - myGenericTLDsMinus = genMinus; - myGenericTLDsPlus = genPlus; - myLocalTLDsMinus = localMinus; - myLocalTLDsPlus = localPlus; - } + public static class Item { + final ArrayType type; + final String[] values; - /** - * Returns true if the specified String parses - * as a valid domain name with a recognized top-level domain. - * The parsing is case-insensitive. - * @param domain the parameter to check for domain name syntax - * @return true if the parameter is a valid domain name - */ - public boolean isValid(String domain) { - if (domain == null) { - return false; - } - domain = unicodeToASCII(domain); - // hosts must be equally reachable via punycode and Unicode - // Unicode is never shorter than punycode, so check punycode - // if domain did not convert, then it will be caught by ASCII - // checks in the regexes below - if (domain.length() > MAX_DOMAIN_LENGTH) { - return false; - } - final String[] groups = domainRegex.match(domain); - if (groups != null && groups.length > 0) { - return isValidTld(groups[0]); + /** + * Constructs a new instance. + * @param type ArrayType, e.g. GENERIC_PLUS, LOCAL_PLUS + * @param values array of TLDs. Will be lower-cased and sorted + */ + public Item(final ArrayType type, final String... values) { + this.type = type; + this.values = values; // no need to copy here } - return allowLocal && hostnameRegex.isValid(domain); } - // package protected for unit test access - // must agree with isValid() above - final boolean isValidDomainSyntax(String domain) { - if (domain == null) { - return false; - } - domain = unicodeToASCII(domain); - // hosts must be equally reachable via punycode and Unicode - // Unicode is never shorter than punycode, so check punycode - // if domain did not convert, then it will be caught by ASCII - // checks in the regexes below - if (domain.length() > MAX_DOMAIN_LENGTH) { - return false; - } - final String[] groups = domainRegex.match(domain); - return (groups != null && groups.length > 0) - || hostnameRegex.isValid(domain); - } + // Regular expression strings for hostnames (derived from RFC2396 and RFC 1123) - /** - * Returns true if the specified String matches any - * IANA-defined top-level domain. Leading dots are ignored if present. - * The search is case-insensitive. - *

- * If allowLocal is true, the TLD is checked using {@link #isValidLocalTld(String)}. - * The TLD is then checked against {@link #isValidInfrastructureTld(String)}, - * {@link #isValidGenericTld(String)} and {@link #isValidCountryCodeTld(String)} - * @param tld the parameter to check for TLD status, not null - * @return true if the parameter is a TLD - */ - public boolean isValidTld(final String tld) { - if(allowLocal && isValidLocalTld(tld)) { - return true; - } - return isValidInfrastructureTld(tld) - || isValidGenericTld(tld) - || isValidCountryCodeTld(tld); - } + private static class LazyHolder { // IODH - /** - * Returns true if the specified String matches any - * IANA-defined infrastructure top-level domain. Leading dots are - * ignored if present. The search is case-insensitive. - * @param iTld the parameter to check for infrastructure TLD status, not null - * @return true if the parameter is an infrastructure TLD - */ - public boolean isValidInfrastructureTld(final String iTld) { - final String key = chompLeadingDot(unicodeToASCII(iTld).toLowerCase(Locale.ENGLISH)); - return arrayContains(INFRASTRUCTURE_TLDS, key); - } + /** + * Singleton instance of this validator, which + * doesn't consider local addresses as valid. + */ + private static final DomainValidator DOMAIN_VALIDATOR = new DomainValidator(false); - /** - * Returns true if the specified String matches any - * IANA-defined generic top-level domain. Leading dots are ignored - * if present. The search is case-insensitive. - * @param gTld the parameter to check for generic TLD status, not null - * @return true if the parameter is a generic TLD - */ - public boolean isValidGenericTld(final String gTld) { - final String key = chompLeadingDot(unicodeToASCII(gTld).toLowerCase(Locale.ENGLISH)); - return (arrayContains(GENERIC_TLDS, key) || arrayContains(myGenericTLDsPlus, key)) - && !arrayContains(myGenericTLDsMinus, key); - } + /** + * Singleton instance of this validator, which does + * consider local addresses valid. + */ + private static final DomainValidator DOMAIN_VALIDATOR_WITH_LOCAL = new DomainValidator(true); - /** - * Returns true if the specified String matches any - * IANA-defined country code top-level domain. Leading dots are - * ignored if present. The search is case-insensitive. - * @param ccTld the parameter to check for country code TLD status, not null - * @return true if the parameter is a country code TLD - */ - public boolean isValidCountryCodeTld(final String ccTld) { - final String key = chompLeadingDot(unicodeToASCII(ccTld).toLowerCase(Locale.ENGLISH)); - return (arrayContains(COUNTRY_CODE_TLDS, key) || arrayContains(myCountryCodeTLDsPlus, key)) - && !arrayContains(myCountryCodeTLDsMinus, key); } - /** - * Returns true if the specified String matches any - * widely used "local" domains (localhost or localdomain). Leading dots are - * ignored if present. The search is case-insensitive. - * @param lTld the parameter to check for local TLD status, not null - * @return true if the parameter is an local TLD - */ - public boolean isValidLocalTld(final String lTld) { - final String key = chompLeadingDot(unicodeToASCII(lTld).toLowerCase(Locale.ENGLISH)); - return (arrayContains(LOCAL_TLDS, key) || arrayContains(myLocalTLDsPlus, key)) - && !arrayContains(myLocalTLDsMinus, key); - } + /** Maximum allowable length ({@value}) of a domain name */ + private static final int MAX_DOMAIN_LENGTH = 253; - /** - * Does this instance allow local addresses? - * - * @return true if local addresses are allowed. - * @since 1.7 - */ - public boolean isAllowLocal() { - return this.allowLocal; - } + private static final String[] EMPTY_STRING_ARRAY = {}; - private String chompLeadingDot(final String str) { - if (str.startsWith(".")) { - return str.substring(1); - } - return str; - } + private static final long serialVersionUID = -4407125112880174009L; - // --------------------------------------------- - // ----- TLDs defined by IANA - // ----- Authoritative and comprehensive list at: - // ----- http://data.iana.org/TLD/tlds-alpha-by-domain.txt + // RFC2396: domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + // Max 63 characters + private static final String DOMAIN_LABEL_REGEX = "\\p{Alnum}(?>[\\p{Alnum}-]{0,61}\\p{Alnum})?"; - // Note that the above list is in UPPER case. - // The code currently converts strings to lower case (as per the tables below) + // RFC2396 toplabel = alpha | alpha *( alphanum | "-" ) alphanum + // Max 63 characters + private static final String TOP_LABEL_REGEX = "\\p{Alpha}(?>[\\p{Alnum}-]{0,61}\\p{Alnum})?"; - // IANA also provide an HTML list at http://www.iana.org/domains/root/db - // Note that this contains several country code entries which are NOT in - // the text file. These all have the "Not assigned" in the "Sponsoring Organisation" column - // For example (as of 2015-01-02): - // .bl country-code Not assigned - // .um country-code Not assigned + /** + * The above instances must only be returned via the getInstance() methods. + * This is to ensure that the override data arrays are properly protected. + */ + + // RFC2396 hostname = *( domainlabel "." ) toplabel [ "." ] + // Note that the regex currently requires both a domain label and a top level label, whereas + // the RFC does not. This is because the regex is used to detect if a TLD is present. + // If the match fails, input is checked against DOMAIN_LABEL_REGEX (hostnameRegex) + // RFC1123 sec 2.1 allows hostnames to start with a digit + private static final String DOMAIN_NAME_REGEX = + "^(?:" + DOMAIN_LABEL_REGEX + "\\.)+" + "(" + TOP_LABEL_REGEX + ")\\.?$"; + private static final String UNEXPECTED_ENUM_VALUE = "Unexpected enum value: "; // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search private static final String[] INFRASTRUCTURE_TLDS = { @@ -1589,6 +1353,7 @@ private String chompLeadingDot(final String str) { "xn--9dbq2a", // קום VeriSign Sarl "xn--9et52u", // 时尚 RISE VICTORY LIMITED "xn--9krt00a", // 微博 Sina Corporation + "xn--9t4b11yi5a", // 테스트 Test "xn--b4w605ferd", // 淡马锡 Temasek Holdings (Private) Limited "xn--bck1b9a5dre4c", // ファッション Amazon Registry Services, Inc. "xn--c1avg", // орг Public Interest Registry @@ -2003,10 +1768,6 @@ private String chompLeadingDot(final String str) { "localdomain", // Also widely used as localhost.localdomain "localhost", // RFC2606 defined }; - - // Additional arrays to supplement or override the built in ones. - // The PLUS arrays are valid keys, the MINUS arrays are invalid keys - /* * This field is used to detect whether the getInstance has been called. * After this, the method updateTLDOverride is not allowed to be called. @@ -2014,25 +1775,26 @@ private String chompLeadingDot(final String str) { * synchronized methods. */ private static boolean inUse; - /* * These arrays are mutable. * They can only be updated by the updateTLDOverride method, and readers must first get an instance - * using the getInstance methods which are all (now) synchronised. - * The only other access is via getTLDEntries which is now synchronised. + * using the getInstance methods which are all (now) synchronized. + * The only other access is via getTLDEntries which is now synchronized. */ // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search private static String[] countryCodeTLDsPlus = EMPTY_STRING_ARRAY; - // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search private static String[] genericTLDsPlus = EMPTY_STRING_ARRAY; - // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search private static String[] countryCodeTLDsMinus = EMPTY_STRING_ARRAY; - // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search private static String[] genericTLDsMinus = EMPTY_STRING_ARRAY; + // N.B. The constructors are deliberately private to avoid possible problems with unsafe publication. + // It is vital that the static override arrays are not mutable once they have been used in an instance + // The arrays could be copied into the instance variables, however if the static array were changed it could + // result in different settings for the shared default instances + // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search private static String[] localTLDsMinus = EMPTY_STRING_ARRAY; @@ -2040,56 +1802,159 @@ private String chompLeadingDot(final String str) { private static String[] localTLDsPlus = EMPTY_STRING_ARRAY; /** - * enum used by {@link DomainValidator#updateTLDOverride(ArrayType, String[])} - * to determine which override array to update / fetch - * @since 1.5.0 - * @since 1.5.1 made public and added read-only array references + * Check if a sorted array contains the specified key + * + * @param sortedArray the array to search + * @param key the key to find + * @return {@code true} if the array contains the key */ - public enum ArrayType { - /** Update (or get a copy of) the GENERIC_TLDS_PLUS table containing additonal generic TLDs */ - GENERIC_PLUS, - /** Update (or get a copy of) the GENERIC_TLDS_MINUS table containing deleted generic TLDs */ - GENERIC_MINUS, - /** Update (or get a copy of) the COUNTRY_CODE_TLDS_PLUS table containing additonal country code TLDs */ - COUNTRY_CODE_PLUS, - /** Update (or get a copy of) the COUNTRY_CODE_TLDS_MINUS table containing deleted country code TLDs */ - COUNTRY_CODE_MINUS, - /** Get a copy of the generic TLDS table */ - GENERIC_RO, - /** Get a copy of the country code table */ - COUNTRY_CODE_RO, - /** Get a copy of the infrastructure table */ - INFRASTRUCTURE_RO, - /** Get a copy of the local table */ - LOCAL_RO, - /** - * Update (or get a copy of) the LOCAL_TLDS_PLUS table containing additional local TLDs - * @since 1.7 - */ - LOCAL_PLUS, - /** - * Update (or get a copy of) the LOCAL_TLDS_MINUS table containing deleted local TLDs - * @since 1.7 - */ - LOCAL_MINUS - ; + private static boolean arrayContains(final String[] sortedArray, final String key) { + return Arrays.binarySearch(sortedArray, key) >= 0; } /** - * Used to specify overrides when creating a new class. + * Returns the singleton instance of this validator. It + * will not consider local addresses as valid. + * @return the singleton instance of this validator + */ + public static synchronized DomainValidator getInstance() { + inUse = true; + return LazyHolder.DOMAIN_VALIDATOR; + } + + /** + * Returns the singleton instance of this validator, + * with local validation as required. + * @param allowLocal Should local addresses be considered valid? + * @return the singleton instance of this validator + */ + public static synchronized DomainValidator getInstance(final boolean allowLocal) { + inUse = true; + if (allowLocal) { + return LazyHolder.DOMAIN_VALIDATOR_WITH_LOCAL; + } + return LazyHolder.DOMAIN_VALIDATOR; + } + + /** + * Returns a new instance of this validator. + * The user can provide a list of {@link Item} entries which can + * be used to override the generic and country code lists. + * Note that any such entries override values provided by the + * {@link #updateTLDOverride(ArrayType, String[])} method + * If an entry for a particular type is not provided, then + * the class override (if any) is retained. + * + * @param allowLocal Should local addresses be considered valid? + * @param items - array of {@link Item} entries + * @return an instance of this validator * @since 1.7 */ - public static class Item { - final ArrayType type; - final String[] values; - /** - * - * @param type ArrayType, e.g. GENERIC_PLUS, LOCAL_PLUS - * @param values array of TLDs. Will be lower-cased and sorted - */ - public Item(final ArrayType type, final String... values) { - this.type = type; - this.values = values; // no need to copy here + public static synchronized DomainValidator getInstance(final boolean allowLocal, final List items) { + inUse = true; + return new DomainValidator(allowLocal, items); + } + + /** + * Gets a copy of a class level internal array. + * @param table the array type (any of the enum values) + * @return a copy of the array + * @throws IllegalArgumentException if the table type is unexpected (should not happen) + * @since 1.5.1 + */ + public static synchronized String[] getTLDEntries(final ArrayType table) { + final String[] array; + switch (table) { + case COUNTRY_CODE_MINUS: + array = countryCodeTLDsMinus; + break; + case COUNTRY_CODE_PLUS: + array = countryCodeTLDsPlus; + break; + case GENERIC_MINUS: + array = genericTLDsMinus; + break; + case GENERIC_PLUS: + array = genericTLDsPlus; + break; + case LOCAL_MINUS: + array = localTLDsMinus; + break; + case LOCAL_PLUS: + array = localTLDsPlus; + break; + case GENERIC_RO: + array = GENERIC_TLDS; + break; + case COUNTRY_CODE_RO: + array = COUNTRY_CODE_TLDS; + break; + case INFRASTRUCTURE_RO: + array = INFRASTRUCTURE_TLDS; + break; + case LOCAL_RO: + array = LOCAL_TLDS; + break; + default: + throw new IllegalArgumentException(UNEXPECTED_ENUM_VALUE + table); + } + return Arrays.copyOf(array, array.length); // clone the array + } + + /* + * Check if input contains only ASCII + * Treats null as all ASCII + */ + private static boolean isOnlyASCII(final String input) { + if (input == null) { + return true; + } + for (int i = 0; i < input.length(); i++) { + if (input.charAt(i) > 0x7F) { // CHECKSTYLE IGNORE MagicNumber + return false; + } + } + return true; + } + + /** + * Converts potentially Unicode input to punycode. + * If conversion fails, returns the original input. + * + * @param input the string to convert, not null + * @return converted input, or original input if conversion fails + */ + // Needed by UrlValidator + static String unicodeToASCII(final String input) { + if (isOnlyASCII(input)) { // skip possibly expensive processing + return input; + } + try { + final String ascii = IDN.toASCII(input); + if (IDNBUGHOLDER.IDN_TOASCII_PRESERVES_TRAILING_DOTS) { + return ascii; + } + final int length = input.length(); + if (length == 0) { // check there is a last character + return input; + } + // RFC3490 3.1. 1) + // Whenever dots are used as label separators, the following + // characters MUST be recognized as dots: U+002E (full stop), U+3002 + // (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61 + // (halfwidth ideographic full stop). + final char lastChar = input.charAt(length - 1);// fetch original last char + switch (lastChar) { + case '\u002E': // "." full stop + case '\u3002': // ideographic full stop + case '\uFF0E': // fullwidth full stop + case '\uFF61': // halfwidth ideographic full stop + return ascii + "."; // restore the missing stop + default: + return ascii; + } + } catch (final IllegalArgumentException e) { // input is not valid + return input; } } @@ -2122,13 +1987,13 @@ public static synchronized void updateTLDOverride(final ArrayType table, final S if (inUse) { throw new IllegalStateException("Can only invoke this method before calling getInstance"); } - final String [] copy = new String[tlds.length]; + final String[] copy = new String[tlds.length]; // Comparisons are always done with lower-case entries for (int i = 0; i < tlds.length; i++) { copy[i] = tlds[i].toLowerCase(Locale.ENGLISH); } Arrays.sort(copy); - switch(table) { + switch (table) { case COUNTRY_CODE_MINUS: countryCodeTLDsMinus = copy; break; @@ -2157,62 +2022,155 @@ public static synchronized void updateTLDOverride(final ArrayType table, final S } } + /** Whether to allow local overrides. */ + private final boolean allowLocal; + + // --------------------------------------------- + // ----- TLDs defined by IANA + // ----- Authoritative and comprehensive list at: + // ----- https://data.iana.org/TLD/tlds-alpha-by-domain.txt + + // Note that the above list is in UPPER case. + // The code currently converts strings to lower case (as per the tables below) + + // IANA also provide an HTML list at http://www.iana.org/domains/root/db + // Note that this contains several country code entries which are NOT in + // the text file. These all have the "Not assigned" in the "Sponsoring Organisation" column + // For example (as of 2015-01-02): + // .bl country-code Not assigned + // .um country-code Not assigned + + /** + * RegexValidator for matching domains. + */ + private final RegexValidator domainRegex = + new RegexValidator(DOMAIN_NAME_REGEX); + + /** + * RegexValidator for matching a local hostname + */ + // RFC1123 sec 2.1 allows hostnames to start with a digit + private final RegexValidator hostnameRegex = + new RegexValidator(DOMAIN_LABEL_REGEX); + + /** Local override. */ + final String[] myCountryCodeTLDsMinus; + + /** Local override. */ + final String[] myCountryCodeTLDsPlus; + + // Additional arrays to supplement or override the built in ones. + // The PLUS arrays are valid keys, the MINUS arrays are invalid keys + + /** Local override. */ + final String[] myGenericTLDsPlus; + + /** Local override. */ + final String[] myGenericTLDsMinus; + + /** Local override. */ + final String[] myLocalTLDsPlus; + + /** Local override. */ + final String[] myLocalTLDsMinus; + + /* + * It is vital that instances are immutable. This is because the default instances are shared. + */ + + /** + * Private constructor. + */ + private DomainValidator(final boolean allowLocal) { + this.allowLocal = allowLocal; + // link to class overrides + myCountryCodeTLDsMinus = countryCodeTLDsMinus; + myCountryCodeTLDsPlus = countryCodeTLDsPlus; + myGenericTLDsPlus = genericTLDsPlus; + myGenericTLDsMinus = genericTLDsMinus; + myLocalTLDsPlus = localTLDsPlus; + myLocalTLDsMinus = localTLDsMinus; + } + /** - * Get a copy of a class level internal array. - * @param table the array type (any of the enum values) - * @return a copy of the array - * @throws IllegalArgumentException if the table type is unexpected (should not happen) - * @since 1.5.1 - */ - public static synchronized String [] getTLDEntries(final ArrayType table) { - final String[] array; - switch(table) { - case COUNTRY_CODE_MINUS: - array = countryCodeTLDsMinus; - break; - case COUNTRY_CODE_PLUS: - array = countryCodeTLDsPlus; - break; - case GENERIC_MINUS: - array = genericTLDsMinus; - break; - case GENERIC_PLUS: - array = genericTLDsPlus; - break; - case LOCAL_MINUS: - array = localTLDsMinus; - break; - case LOCAL_PLUS: - array = localTLDsPlus; - break; - case GENERIC_RO: - array = GENERIC_TLDS; - break; - case COUNTRY_CODE_RO: - array = COUNTRY_CODE_TLDS; - break; - case INFRASTRUCTURE_RO: - array = INFRASTRUCTURE_TLDS; - break; - case LOCAL_RO: - array = LOCAL_TLDS; - break; - default: - throw new IllegalArgumentException(UNEXPECTED_ENUM_VALUE + table); + * Private constructor, allowing local overrides + * @since 1.7 + */ + private DomainValidator(final boolean allowLocal, final List items) { + this.allowLocal = allowLocal; + + // default to class overrides + String[] ccMinus = countryCodeTLDsMinus; + String[] ccPlus = countryCodeTLDsPlus; + String[] genMinus = genericTLDsMinus; + String[] genPlus = genericTLDsPlus; + String[] localMinus = localTLDsMinus; + String[] localPlus = localTLDsPlus; + + // apply the instance overrides + for (final Item item : items) { + final String[] copy = new String[item.values.length]; + // Comparisons are always done with lower-case entries + for (int i = 0; i < item.values.length; i++) { + copy[i] = item.values[i].toLowerCase(Locale.ENGLISH); + } + Arrays.sort(copy); + switch (item.type) { + case COUNTRY_CODE_MINUS: { + ccMinus = copy; + break; + } + case COUNTRY_CODE_PLUS: { + ccPlus = copy; + break; + } + case GENERIC_MINUS: { + genMinus = copy; + break; + } + case GENERIC_PLUS: { + genPlus = copy; + break; + } + case LOCAL_MINUS: { + localMinus = copy; + break; + } + case LOCAL_PLUS: { + localPlus = copy; + break; + } + default: + break; + } } - return Arrays.copyOf(array, array.length); // clone the array + + // init the instance overrides + myCountryCodeTLDsMinus = ccMinus; + myCountryCodeTLDsPlus = ccPlus; + myGenericTLDsMinus = genMinus; + myGenericTLDsPlus = genPlus; + myLocalTLDsMinus = localMinus; + myLocalTLDsPlus = localPlus; + } + + private String chompLeadingDot(final String str) { + if (str.startsWith(".")) { + return str.substring(1); + } + return str; } /** - * Get a copy of an instance level internal array. + * Gets a copy of an instance level internal array. * @param table the array type (any of the enum values) * @return a copy of the array * @throws IllegalArgumentException if the table type is unexpected, e.g. GENERIC_RO * @since 1.7 */ - public String [] getOverrides(final ArrayType table) { + public String[] getOverrides(final ArrayType table) { final String[] array; - switch(table) { + switch (table) { case COUNTRY_CODE_MINUS: array = myCountryCodeTLDsMinus; break; @@ -2236,79 +2194,126 @@ public static synchronized void updateTLDOverride(final ArrayType table, final S } return Arrays.copyOf(array, array.length); // clone the array } + /** - * Converts potentially Unicode input to punycode. - * If conversion fails, returns the original input. + * Does this instance allow local addresses? * - * @param input the string to convert, not null - * @return converted input, or original input if conversion fails + * @return true if local addresses are allowed. + * @since 1.7 */ - // Needed by UrlValidator - static String unicodeToASCII(final String input) { - if (isOnlyASCII(input)) { // skip possibly expensive processing - return input; - } - try { - final String ascii = IDN.toASCII(input); - if (IDNBUGHOLDER.IDN_TOASCII_PRESERVES_TRAILING_DOTS) { - return ascii; - } - final int length = input.length(); - if (length == 0) {// check there is a last character - return input; - } - // RFC3490 3.1. 1) - // Whenever dots are used as label separators, the following - // characters MUST be recognized as dots: U+002E (full stop), U+3002 - // (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61 - // (halfwidth ideographic full stop). - final char lastChar = input.charAt(length-1);// fetch original last char - switch(lastChar) { - case '\u002E': // "." full stop - case '\u3002': // ideographic full stop - case '\uFF0E': // fullwidth full stop - case '\uFF61': // halfwidth ideographic full stop - return ascii + "."; // restore the missing stop - default: - return ascii; - } - } catch (final IllegalArgumentException e) { // input is not valid - return input; - } + public boolean isAllowLocal() { + return this.allowLocal; } - private static class IDNBUGHOLDER { - private static boolean keepsTrailingDot() { - final String input = "a."; // must be a valid name - return input.equals(IDN.toASCII(input)); + /** + * Returns true if the specified String parses + * as a valid domain name with a recognized top-level domain. + * The parsing is case-insensitive. + * @param domain the parameter to check for domain name syntax + * @return true if the parameter is a valid domain name + */ + public boolean isValid(String domain) { + if (domain == null) { + return false; } - private static final boolean IDN_TOASCII_PRESERVES_TRAILING_DOTS = keepsTrailingDot(); + domain = unicodeToASCII(domain); + // hosts must be equally reachable via punycode and Unicode + // Unicode is never shorter than punycode, so check punycode + // if domain did not convert, then it will be caught by ASCII + // checks in the regexes below + if (domain.length() > MAX_DOMAIN_LENGTH) { + return false; + } + final String[] groups = domainRegex.match(domain); + if (groups != null && groups.length > 0) { + return isValidTld(groups[0]); + } + return allowLocal && hostnameRegex.isValid(domain); } - /* - * Check if input contains only ASCII - * Treats null as all ASCII + /** + * Returns true if the specified String matches any + * IANA-defined country code top-level domain. Leading dots are + * ignored if present. The search is case-insensitive. + * @param ccTld the parameter to check for country code TLD status, not null + * @return true if the parameter is a country code TLD */ - private static boolean isOnlyASCII(final String input) { - if (input == null) { - return true; + public boolean isValidCountryCodeTld(final String ccTld) { + final String key = chompLeadingDot(unicodeToASCII(ccTld).toLowerCase(Locale.ENGLISH)); + return (arrayContains(COUNTRY_CODE_TLDS, key) || arrayContains(myCountryCodeTLDsPlus, key)) && !arrayContains(myCountryCodeTLDsMinus, key); + } + + // package protected for unit test access + // must agree with isValid() above + final boolean isValidDomainSyntax(String domain) { + if (domain == null) { + return false; } - for(int i=0; i < input.length(); i++) { - if (input.charAt(i) > 0x7F) { // CHECKSTYLE IGNORE MagicNumber - return false; - } + domain = unicodeToASCII(domain); + // hosts must be equally reachable via punycode and Unicode + // Unicode is never shorter than punycode, so check punycode + // if domain did not convert, then it will be caught by ASCII + // checks in the regexes below + if (domain.length() > MAX_DOMAIN_LENGTH) { + return false; } - return true; + final String[] groups = domainRegex.match(domain); + return groups != null && groups.length > 0 || hostnameRegex.isValid(domain); + } + /** + * Returns true if the specified String matches any + * IANA-defined generic top-level domain. Leading dots are ignored + * if present. The search is case-insensitive. + * @param gTld the parameter to check for generic TLD status, not null + * @return true if the parameter is a generic TLD + */ + public boolean isValidGenericTld(final String gTld) { + final String key = chompLeadingDot(unicodeToASCII(gTld).toLowerCase(Locale.ENGLISH)); + return (arrayContains(GENERIC_TLDS, key) || arrayContains(myGenericTLDsPlus, key)) && !arrayContains(myGenericTLDsMinus, key); } /** - * Check if a sorted array contains the specified key - * - * @param sortedArray the array to search - * @param key the key to find - * @return {@code true} if the array contains the key + * Returns true if the specified String matches any + * IANA-defined infrastructure top-level domain. Leading dots are + * ignored if present. The search is case-insensitive. + * @param iTld the parameter to check for infrastructure TLD status, not null + * @return true if the parameter is an infrastructure TLD */ - private static boolean arrayContains(final String[] sortedArray, final String key) { - return Arrays.binarySearch(sortedArray, key) >= 0; + public boolean isValidInfrastructureTld(final String iTld) { + final String key = chompLeadingDot(unicodeToASCII(iTld).toLowerCase(Locale.ENGLISH)); + return arrayContains(INFRASTRUCTURE_TLDS, key); + } + + /** + * Returns true if the specified String matches any + * widely used "local" domains (localhost or localdomain). Leading dots are + * ignored if present. The search is case-insensitive. + * @param lTld the parameter to check for local TLD status, not null + * @return true if the parameter is an local TLD + */ + public boolean isValidLocalTld(final String lTld) { + final String key = chompLeadingDot(unicodeToASCII(lTld).toLowerCase(Locale.ENGLISH)); + return (arrayContains(LOCAL_TLDS, key) || arrayContains(myLocalTLDsPlus, key)) + && !arrayContains(myLocalTLDsMinus, key); + } + + /** + * Returns true if the specified String matches any + * IANA-defined top-level domain. Leading dots are ignored if present. + * The search is case-insensitive. + *

+ * If allowLocal is true, the TLD is checked using {@link #isValidLocalTld(String)}. + * The TLD is then checked against {@link #isValidInfrastructureTld(String)}, + * {@link #isValidGenericTld(String)} and {@link #isValidCountryCodeTld(String)} + * @param tld the parameter to check for TLD status, not null + * @return true if the parameter is a TLD + */ + public boolean isValidTld(final String tld) { + if (allowLocal && isValidLocalTld(tld)) { + return true; + } + return isValidInfrastructureTld(tld) + || isValidGenericTld(tld) + || isValidCountryCodeTld(tld); } } \ No newline at end of file diff --git a/src/main/java/com/networknt/org/apache/commons/validator/routines/EmailValidator.java b/src/main/java/com/networknt/org/apache/commons/validator/routines/EmailValidator.java index ecf56329f..6876d7959 100644 --- a/src/main/java/com/networknt/org/apache/commons/validator/routines/EmailValidator.java +++ b/src/main/java/com/networknt/org/apache/commons/validator/routines/EmailValidator.java @@ -24,7 +24,7 @@ *

Perform email validations.

*

* Based on a script by Sandeep V. Tamhankar - * http://javascript.internet.com + * https://javascript.internet.com *

*

* This implementation is not guaranteed to catch all possible errors in an email address. @@ -51,8 +51,6 @@ public class EmailValidator implements Serializable { private static final int MAX_USERNAME_LEN = 64; - private final boolean allowTld; - /** * Singleton instance of this class, which * doesn't consider local addresses as valid. @@ -71,15 +69,12 @@ public class EmailValidator implements Serializable { */ private static final EmailValidator EMAIL_VALIDATOR_WITH_LOCAL = new EmailValidator(true, false); - /** * Singleton instance of this class, which does * consider local addresses valid. */ private static final EmailValidator EMAIL_VALIDATOR_WITH_LOCAL_WITH_TLD = new EmailValidator(true, true); - private final DomainValidator domainValidator; - /** * Returns the Singleton instance of this validator. * @@ -89,6 +84,17 @@ public static EmailValidator getInstance() { return EMAIL_VALIDATOR; } + /** + * Returns the Singleton instance of this validator, + * with local validation as required. + * + * @param allowLocal Should local addresses be considered valid? + * @return singleton instance of this validator + */ + public static EmailValidator getInstance(final boolean allowLocal) { + return getInstance(allowLocal, false); + } + /** * Returns the Singleton instance of this validator, * with local validation as required. @@ -110,15 +116,28 @@ public static EmailValidator getInstance(final boolean allowLocal, final boolean return EMAIL_VALIDATOR; } + private final boolean allowTld; + + private final DomainValidator domainValidator; + /** - * Returns the Singleton instance of this validator, - * with local validation as required. + * Protected constructor for subclasses to use. * * @param allowLocal Should local addresses be considered valid? - * @return singleton instance of this validator */ - public static EmailValidator getInstance(final boolean allowLocal) { - return getInstance(allowLocal, false); + protected EmailValidator(final boolean allowLocal) { + this(allowLocal, false); + } + + /** + * Protected constructor for subclasses to use. + * + * @param allowLocal Should local addresses be considered valid? + * @param allowTld Should TLDs be allowed? + */ + protected EmailValidator(final boolean allowLocal, final boolean allowTld) { + this.allowTld = allowTld; + this.domainValidator = DomainValidator.getInstance(allowLocal); } /** @@ -141,30 +160,10 @@ public EmailValidator(final boolean allowLocal, final boolean allowTld, final Do this.domainValidator = domainValidator; } - /** - * Protected constructor for subclasses to use. - * - * @param allowLocal Should local addresses be considered valid? - * @param allowTld Should TLDs be allowed? - */ - protected EmailValidator(final boolean allowLocal, final boolean allowTld) { - this.allowTld = allowTld; - this.domainValidator = DomainValidator.getInstance(allowLocal); - } - - /** - * Protected constructor for subclasses to use. - * - * @param allowLocal Should local addresses be considered valid? - */ - protected EmailValidator(final boolean allowLocal) { - this(allowLocal, false); - } - /** *

Checks if a field has a valid e-mail address.

* - * @param email The value validation is being performed on. A null + * @param email The value validation is being performed on. A {@code null} * value is considered invalid. * @return true if the email address is valid. */ @@ -211,7 +210,7 @@ protected boolean isValidDomain(final String domain) { } // Domain is symbolic name if (allowTld) { - return domainValidator.isValid(domain) || (!domain.startsWith(".") && domainValidator.isValidTld(domain)); + return domainValidator.isValid(domain) || !domain.startsWith(".") && domainValidator.isValidTld(domain); } return domainValidator.isValid(domain); } diff --git a/src/main/java/com/networknt/org/apache/commons/validator/routines/InetAddressValidator.java b/src/main/java/com/networknt/org/apache/commons/validator/routines/InetAddressValidator.java index 5036654f2..f3ed5aeba 100644 --- a/src/main/java/com/networknt/org/apache/commons/validator/routines/InetAddressValidator.java +++ b/src/main/java/com/networknt/org/apache/commons/validator/routines/InetAddressValidator.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.regex.Pattern; /** *

InetAddress validation and conversion routines (java.net.InetAddress).

@@ -59,59 +60,22 @@ public class InetAddressValidator implements Serializable { */ private static final InetAddressValidator VALIDATOR = new InetAddressValidator(); - /** - * IPv4 RegexValidator - */ - private final RegexValidator ipv4Validator = new RegexValidator(IPV4_REGEX); + private static final Pattern DIGITS_PATTERN = Pattern.compile("\\d{1,3}"); + private static final Pattern ID_CHECK_PATTERN = Pattern.compile("[^\\s/%]+"); /** * Returns the singleton instance of this validator. - * * @return the singleton instance of this validator */ public static InetAddressValidator getInstance() { return VALIDATOR; } - /** - * This method parse and validate the given inet6Address and return the valid inet6Address Part - * - * @param inet6Address inet6Address to be processed - * @return valid part of inet6Address after processing - */ - private static String getValidInet6AddressPart(String inet6Address) { - String[] parts; - // remove prefix size. This will appear after the zone id (if any) - parts = inet6Address.split("/", -1); - if (parts.length > 2) { - return null; - } - if (parts.length == 2) { - if (!parts[1].matches("\\d{1,3}")) { - return null; - } - final int bits = Integer.parseInt(parts[1]); // cannot fail because of RE check - if (bits < 0 || bits > MAX_BYTE) { - return null; - } - } - // remove zone-id - parts = parts[0].split("%", -1); - if (parts.length > 2) { - return null; - } - // The id syntax is implementation independent, but it presumably cannot allow: - // whitespace, '/' or '%' - if ((parts.length == 2) && !parts[1].matches("[^\\s/%]+")) { - return null; - } - inet6Address = parts[0]; - return inet6Address; - } + /** IPv4 RegexValidator */ + private final RegexValidator ipv4Validator = new RegexValidator(IPV4_REGEX); /** * Checks if the specified string is a valid IPv4 or IPv6 address. - * * @param inetAddress the string to validate * @return true if the string validates as an IP address */ @@ -121,7 +85,6 @@ public boolean isValid(final String inetAddress) { /** * Validates an IPv4 address. Returns true if valid. - * * @param inet4Address the IPv4 address to validate * @return true if the argument contains a valid IPv4 address */ @@ -162,20 +125,44 @@ public boolean isValidInet4Address(final String inet4Address) { /** * Validates an IPv6 address. Returns true if valid. - * * @param inet6Address the IPv6 address to validate * @return true if the argument contains a valid IPv6 address + * * @since 1.4.1 */ public boolean isValidInet6Address(String inet6Address) { - inet6Address = getValidInet6AddressPart(inet6Address); - if (inet6Address == null) return false; // invalid id + String[] parts; + // remove prefix size. This will appear after the zone id (if any) + parts = inet6Address.split("/", -1); + if (parts.length > 2) { + return false; // can only have one prefix specifier + } + if (parts.length == 2) { + if (!DIGITS_PATTERN.matcher(parts[1]).matches()) { + return false; // not a valid number + } + final int bits = Integer.parseInt(parts[1]); // cannot fail because of RE check + if (bits < 0 || bits > MAX_BYTE) { + return false; // out of range + } + } + // remove zone-id + parts = parts[0].split("%", -1); + if (parts.length > 2) { + return false; + } + // The id syntax is implementation independent, but it presumably cannot allow: + // whitespace, '/' or '%' + if (parts.length == 2 && !ID_CHECK_PATTERN.matcher(parts[1]).matches()) { + return false; // invalid id + } + inet6Address = parts[0]; final boolean containsCompressedZeroes = inet6Address.contains("::"); - if (containsCompressedZeroes && (inet6Address.indexOf("::") != inet6Address.lastIndexOf("::"))) { + if (containsCompressedZeroes && inet6Address.indexOf("::") != inet6Address.lastIndexOf("::")) { return false; } - if ((inet6Address.startsWith(":") && !inet6Address.startsWith("::")) - || (inet6Address.endsWith(":") && !inet6Address.endsWith("::"))) { + if (inet6Address.startsWith(":") && !inet6Address.startsWith("::") + || inet6Address.endsWith(":") && !inet6Address.endsWith("::")) { return false; } String[] octets = inet6Address.split(":"); @@ -226,6 +213,9 @@ public boolean isValidInet6Address(String inet6Address) { } validOctets++; } - return validOctets <= IPV6_MAX_HEX_GROUPS && (validOctets >= IPV6_MAX_HEX_GROUPS || containsCompressedZeroes); + if (validOctets > IPV6_MAX_HEX_GROUPS || validOctets < IPV6_MAX_HEX_GROUPS && !containsCompressedZeroes) { + return false; + } + return true; } } \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index de765030c..06f9d69f3 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -50,12 +50,14 @@ static PatternFormat pattern(String name, String regex) { } public static final List COMMON_BUILTIN_FORMATS = new ArrayList<>(); + + public static final String IPV6_PATTERN = "^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$"; // this section contains formats common to all dialects. static { COMMON_BUILTIN_FORMATS.add(pattern("hostname", "^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]))*$", "must be a valid RFC 1123 host name")); COMMON_BUILTIN_FORMATS.add(pattern("ipv4", "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$", "must be a valid RFC 2673 IP address")); - COMMON_BUILTIN_FORMATS.add(pattern("ipv6", "^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$", "must be a valid RFC 4291 IP address")); + COMMON_BUILTIN_FORMATS.add(pattern("ipv6", IPV6_PATTERN, "must be a valid RFC 4291 IP address")); COMMON_BUILTIN_FORMATS.add(pattern("json-pointer", "^(/([^/#~]|[~](?=[01]))*)*$", "must be a valid RFC 6901 JSON Pointer")); COMMON_BUILTIN_FORMATS.add(pattern("relative-json-pointer", "^(0|([1-9]\\d*))(#|(/([^/#~]|[~](?=[01]))*)*)$", "must be a valid IETF Relative JSON Pointer")); COMMON_BUILTIN_FORMATS.add(pattern("uri-template", "^([^\\p{Cntrl}\"'%<>\\^`\\{|\\}]|%\\p{XDigit}{2}|\\{[+#./;?&=,!@|]?((\\w|%\\p{XDigit}{2})(\\.?(\\w|%\\p{XDigit}{2}))*(:[1-9]\\d{0,3}|\\*)?)(,((\\w|%\\p{XDigit}{2})(\\.?(\\w|%\\p{XDigit}{2}))*(:[1-9]\\d{0,3}|\\*)?))*\\})*$", "must be a valid RFC 6570 URI Template")); diff --git a/src/main/java/com/networknt/schema/format/IriFormat.java b/src/main/java/com/networknt/schema/format/IriFormat.java index 4e24ad381..540bbceb5 100644 --- a/src/main/java/com/networknt/schema/format/IriFormat.java +++ b/src/main/java/com/networknt/schema/format/IriFormat.java @@ -1,8 +1,12 @@ package com.networknt.schema.format; import java.net.URI; +import java.util.regex.Pattern; + +import com.networknt.schema.JsonMetaSchema; public class IriFormat extends AbstractRFC3986Format { + private static final Pattern IPV6_PATTERN = Pattern.compile(JsonMetaSchema.IPV6_PATTERN); public IriFormat() { super("iri", "must be a valid RFC 3987 IRI"); @@ -10,7 +14,16 @@ public IriFormat() { @Override protected boolean validate(URI uri) { - return uri.isAbsolute(); + boolean result = uri.isAbsolute(); + if (result) { + String authority = uri.getAuthority(); + if (authority != null) { + if (IPV6_PATTERN.matcher(authority).matches() ) { + return false; + } + } + } + return result; } } diff --git a/src/main/java/com/networknt/schema/utils/RFC5892.java b/src/main/java/com/networknt/schema/utils/RFC5892.java index bd71193b3..0639daaea 100644 --- a/src/main/java/com/networknt/schema/utils/RFC5892.java +++ b/src/main/java/com/networknt/schema/utils/RFC5892.java @@ -123,6 +123,10 @@ public static boolean isValid(String value) { case DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: rules = IDNA_RULES.and(RTL); break; + case DIRECTIONALITY_EUROPEAN_NUMBER: + case DIRECTIONALITY_OTHER_NEUTRALS: + rules = IDNA_RULES; + break; default: return false; } @@ -269,7 +273,7 @@ private static boolean testKatakanaMiddleDot(String s, int i) { // There must be a Katakana, Hiragana or Han character after this symbol if (s.length() == 1 + i) return false; int following = s.codePointAt(i + 1); - if (!isKatakana(following)) return false; + if (!(isKatakana(following))) return false; } return true; } diff --git a/src/test/suite/tests/draft2019-09/optional/format/hostname.json b/src/test/suite/tests/draft2019-09/optional/format/hostname.json index eac8cac6f..f3b7181c8 100644 --- a/src/test/suite/tests/draft2019-09/optional/format/hostname.json +++ b/src/test/suite/tests/draft2019-09/optional/format/hostname.json @@ -95,6 +95,31 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft2019-09/optional/format/idn-email.json b/src/test/suite/tests/draft2019-09/optional/format/idn-email.json index 40672aaad..baf01d0aa 100644 --- a/src/test/suite/tests/draft2019-09/optional/format/idn-email.json +++ b/src/test/suite/tests/draft2019-09/optional/format/idn-email.json @@ -38,7 +38,7 @@ }, { "description": "a valid idn e-mail (example@example.test in Hangul)", - "data": "실례@실례.닷컴", + "data": "실례@실례.테스트", "valid": true }, { diff --git a/src/test/suite/tests/draft2019-09/optional/format/idn-hostname.json b/src/test/suite/tests/draft2019-09/optional/format/idn-hostname.json index 7d9e325ac..072a6b08e 100644 --- a/src/test/suite/tests/draft2019-09/optional/format/idn-hostname.json +++ b/src/test/suite/tests/draft2019-09/optional/format/idn-hostname.json @@ -239,25 +239,25 @@ { "description": "KATAKANA MIDDLE DOT with Hiragana", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", - "data": "\u3045\u30fb\u3041", + "data": "\u30fb\u3041", "valid": true }, { "description": "KATAKANA MIDDLE DOT with Katakana", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", - "data": "\u30a1\u30fb\u30a1", + "data": "\u30fb\u30a1", "valid": true }, { "description": "KATAKANA MIDDLE DOT with Han", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", - "data": "\u4e09\u30fb\u4e08", + "data": "\u30fb\u4e08", "valid": true }, { "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", - "data": "\u0624\u0660\u06f0", + "data": "\u0660\u06f0", "valid": false }, { @@ -269,7 +269,7 @@ { "description": "Extended Arabic-Indic digits not mixed with Arabic-Indic digits", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.9", - "data": "\u0624\u06f00", + "data": "\u06f00", "valid": true }, { @@ -301,6 +301,31 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft2020-12/optional/format/hostname.json b/src/test/suite/tests/draft2020-12/optional/format/hostname.json index c8db9770e..41418dd4a 100644 --- a/src/test/suite/tests/draft2020-12/optional/format/hostname.json +++ b/src/test/suite/tests/draft2020-12/optional/format/hostname.json @@ -95,6 +95,31 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft2020-12/optional/format/idn-email.json b/src/test/suite/tests/draft2020-12/optional/format/idn-email.json index e532433b0..50f3c23ce 100644 --- a/src/test/suite/tests/draft2020-12/optional/format/idn-email.json +++ b/src/test/suite/tests/draft2020-12/optional/format/idn-email.json @@ -38,7 +38,7 @@ }, { "description": "a valid idn e-mail (example@example.test in Hangul)", - "data": "실례@실례.닷컴", + "data": "실례@실례.테스트", "valid": true }, { diff --git a/src/test/suite/tests/draft2020-12/optional/format/idn-hostname.json b/src/test/suite/tests/draft2020-12/optional/format/idn-hostname.json index 72e552464..bc7d92f66 100644 --- a/src/test/suite/tests/draft2020-12/optional/format/idn-hostname.json +++ b/src/test/suite/tests/draft2020-12/optional/format/idn-hostname.json @@ -239,25 +239,25 @@ { "description": "KATAKANA MIDDLE DOT with Hiragana", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", - "data": "\u3045\u30fb\u3041", + "data": "\u30fb\u3041", "valid": true }, { "description": "KATAKANA MIDDLE DOT with Katakana", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", - "data": "\u30a1\u30fb\u30a1", + "data": "\u30fb\u30a1", "valid": true }, { "description": "KATAKANA MIDDLE DOT with Han", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", - "data": "\u4e09\u30fb\u4e08", + "data": "\u30fb\u4e08", "valid": true }, { "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", - "data": "\u0624\u0660\u06f0", + "data": "\u0660\u06f0", "valid": false }, { @@ -269,7 +269,7 @@ { "description": "Extended Arabic-Indic digits not mixed with Arabic-Indic digits", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.9", - "data": "\u0624\u06f00", + "data": "\u06f00", "valid": true }, { @@ -301,6 +301,31 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft4/optional/format/hostname.json b/src/test/suite/tests/draft4/optional/format/hostname.json index 8a67fda88..a8ecd194f 100644 --- a/src/test/suite/tests/draft4/optional/format/hostname.json +++ b/src/test/suite/tests/draft4/optional/format/hostname.json @@ -92,6 +92,26 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft4/optional/format/ipv4.json b/src/test/suite/tests/draft4/optional/format/ipv4.json index 4706581f2..9680fe620 100644 --- a/src/test/suite/tests/draft4/optional/format/ipv4.json +++ b/src/test/suite/tests/draft4/optional/format/ipv4.json @@ -78,6 +78,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/src/test/suite/tests/draft6/optional/format/hostname.json b/src/test/suite/tests/draft6/optional/format/hostname.json index 8a67fda88..a8ecd194f 100644 --- a/src/test/suite/tests/draft6/optional/format/hostname.json +++ b/src/test/suite/tests/draft6/optional/format/hostname.json @@ -92,6 +92,26 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft6/optional/format/ipv4.json b/src/test/suite/tests/draft6/optional/format/ipv4.json index 4706581f2..9680fe620 100644 --- a/src/test/suite/tests/draft6/optional/format/ipv4.json +++ b/src/test/suite/tests/draft6/optional/format/ipv4.json @@ -78,6 +78,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/src/test/suite/tests/draft7/optional/format/hostname.json b/src/test/suite/tests/draft7/optional/format/hostname.json index 8a67fda88..a8ecd194f 100644 --- a/src/test/suite/tests/draft7/optional/format/hostname.json +++ b/src/test/suite/tests/draft7/optional/format/hostname.json @@ -92,6 +92,26 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft7/optional/format/idn-email.json b/src/test/suite/tests/draft7/optional/format/idn-email.json index 389dd24ec..6e213745a 100644 --- a/src/test/suite/tests/draft7/optional/format/idn-email.json +++ b/src/test/suite/tests/draft7/optional/format/idn-email.json @@ -35,7 +35,7 @@ }, { "description": "a valid idn e-mail (example@example.test in Hangul)", - "data": "실례@실례.닷컴", + "data": "실례@실례.테스트", "valid": true }, { diff --git a/src/test/suite/tests/draft7/optional/format/idn-hostname.json b/src/test/suite/tests/draft7/optional/format/idn-hostname.json index 6de6d1388..dc47f7b5c 100644 --- a/src/test/suite/tests/draft7/optional/format/idn-hostname.json +++ b/src/test/suite/tests/draft7/optional/format/idn-hostname.json @@ -236,25 +236,25 @@ { "description": "KATAKANA MIDDLE DOT with Hiragana", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", - "data": "\u3045\u30fb\u3041", + "data": "\u30fb\u3041", "valid": true }, { "description": "KATAKANA MIDDLE DOT with Katakana", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", - "data": "\u30a1\u30fb\u30a1", + "data": "\u30fb\u30a1", "valid": true }, { "description": "KATAKANA MIDDLE DOT with Han", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", - "data": "\u4e09\u30fb\u4e08", + "data": "\u30fb\u4e08", "valid": true }, { "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", - "data": "\u0624\u0660\u06f0", + "data": "\u0660\u06f0", "valid": false }, { @@ -266,7 +266,7 @@ { "description": "Extended Arabic-Indic digits not mixed with Arabic-Indic digits", "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.9", - "data": "\u0624\u06f00", + "data": "\u06f00", "valid": true }, { @@ -298,6 +298,26 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft7/optional/format/iri.json b/src/test/suite/tests/draft7/optional/format/iri.json index 4bef9ebb2..a0d12aed6 100644 --- a/src/test/suite/tests/draft7/optional/format/iri.json +++ b/src/test/suite/tests/draft7/optional/format/iri.json @@ -61,9 +61,7 @@ { "description": "an invalid IRI based on IPv6", "data": "http://2001:0db8:85a3:0000:0000:8a2e:0370:7334", - "valid": false, - "disabled": true, - "reason": "URI syntax cannot always distinguish a malformed server-based authority from a legitimate registry-based authority" + "valid": false }, { "description": "an invalid relative IRI Reference", From 02d47a1ba2b2c1fa586cbaae17a9fa189ca80d40 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 26 Jan 2024 16:09:04 +0800 Subject: [PATCH 63/65] Update doc --- doc/compatibility.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/compatibility.md b/doc/compatibility.md index f5119484d..f20c97e2c 100644 --- a/doc/compatibility.md +++ b/doc/compatibility.md @@ -97,6 +97,17 @@ This is not ECMA-262 compliant and is thus not compliant with the JSON Schema sp The library can be configured to use a ECMA-262 compliant regular expression validator which is implemented using [joni](https://github.com/jruby/joni). This can be configured by setting `setEcma262Validator` to `true`. +This also requires adding the `joni` dependency. + +```xml + + + org.jruby.joni + joni + ${version.joni} + +``` + ### Format Since Draft 2019-09 the `format` keyword only generates annotations by default and does not generate assertions. From 63e3e1e608f367b2f54244afe415dd70e2eb1dc8 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 26 Jan 2024 16:30:12 +0800 Subject: [PATCH 64/65] Update doc --- doc/schema-retrieval.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/doc/schema-retrieval.md b/doc/schema-retrieval.md index e253699b7..697a974c8 100644 --- a/doc/schema-retrieval.md +++ b/doc/schema-retrieval.md @@ -59,20 +59,24 @@ public class CustomUriSchemaLoader implements SchemaLoader { @Override public InputStreamSource getSchema(AbsoluteIri absoluteIri) { - URI uri = URI.create(absoluteIri.toString()); - return () -> { - HttpRequest request = HttpRequest.newBuilder().uri(uri).header("Authorization", authorizationToken).build(); - try { - HttpResponse response = this.client.send(request, HttpResponse.BodyHandlers.ofString()); - if ((200 > response.statusCode()) || (response.statusCode() > 299)) { - String errorMessage = String.format("Could not get data from schema endpoint. The following status %d was returned.", response.statusCode()); - LOGGER.error(errorMessage); + String scheme = absoluteIri.getScheme(); + if ("https".equals(scheme) || "http".equals(scheme)) { + URI uri = URI.create(absoluteIri.toString()); + return () -> { + HttpRequest request = HttpRequest.newBuilder().uri(uri).header("Authorization", authorizationToken).build(); + try { + HttpResponse response = this.client.send(request, HttpResponse.BodyHandlers.ofString()); + if ((200 > response.statusCode()) || (response.statusCode() > 299)) { + String errorMessage = String.format("Could not get data from schema endpoint. The following status %d was returned.", response.statusCode()); + LOGGER.error(errorMessage); + } + return new ByteArrayInputStream(response.body().getBytes(StandardCharsets.UTF_8)); + } catch (InterruptedException e) { + throw new RuntimeException(e); } - return new ByteArrayInputStream(response.body().getBytes(StandardCharsets.UTF_8)); - } catch (InterruptedException e) { - throw new RuntimeException(e); } } + return null; } } ``` From 90fdfd05f15f94e8a2598d2533312d05c62d08e6 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 26 Jan 2024 16:33:28 +0800 Subject: [PATCH 65/65] Fix spacing --- src/main/java/com/networknt/schema/format/IriFormat.java | 2 +- src/main/java/com/networknt/schema/utils/RFC5892.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/networknt/schema/format/IriFormat.java b/src/main/java/com/networknt/schema/format/IriFormat.java index 540bbceb5..02475a530 100644 --- a/src/main/java/com/networknt/schema/format/IriFormat.java +++ b/src/main/java/com/networknt/schema/format/IriFormat.java @@ -14,7 +14,7 @@ public IriFormat() { @Override protected boolean validate(URI uri) { - boolean result = uri.isAbsolute(); + boolean result = uri.isAbsolute(); if (result) { String authority = uri.getAuthority(); if (authority != null) { diff --git a/src/main/java/com/networknt/schema/utils/RFC5892.java b/src/main/java/com/networknt/schema/utils/RFC5892.java index 0639daaea..76d044436 100644 --- a/src/main/java/com/networknt/schema/utils/RFC5892.java +++ b/src/main/java/com/networknt/schema/utils/RFC5892.java @@ -273,7 +273,7 @@ private static boolean testKatakanaMiddleDot(String s, int i) { // There must be a Katakana, Hiragana or Han character after this symbol if (s.length() == 1 + i) return false; int following = s.codePointAt(i + 1); - if (!(isKatakana(following))) return false; + if (!isKatakana(following)) return false; } return true; }