All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.networknt.schema.JsonSchemaFactory Maven / Gradle / Ivy

There is a newer version: 1.5.3
Show newest version
/*
 * Copyright (c) 2016 Network New Technologies Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.networknt.schema;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.SpecVersion.VersionFlag;
import com.networknt.schema.resource.DefaultSchemaLoader;
import com.networknt.schema.resource.SchemaLoader;
import com.networknt.schema.resource.SchemaLoaders;
import com.networknt.schema.resource.SchemaMapper;
import com.networknt.schema.resource.SchemaMappers;
import com.networknt.schema.serialization.JsonMapperFactory;
import com.networknt.schema.serialization.YamlMapperFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;

/**
 * Factory for building {@link JsonSchema} instances.
 * 

* The factory should be typically be created using * {@link #getInstance(VersionFlag, Consumer)}. */ public class JsonSchemaFactory { private static final Logger logger = LoggerFactory .getLogger(JsonSchemaFactory.class); public static class Builder { private ObjectMapper jsonMapper = null; private ObjectMapper yamlMapper = null; private String defaultMetaSchemaURI; private final ConcurrentMap jsonMetaSchemas = new ConcurrentHashMap(); private SchemaLoaders.Builder schemaLoadersBuilder = null; private SchemaMappers.Builder schemaMappersBuilder = null; private boolean enableUriSchemaCache = true; public Builder jsonMapper(final ObjectMapper jsonMapper) { this.jsonMapper = jsonMapper; return this; } public Builder yamlMapper(final ObjectMapper yamlMapper) { this.yamlMapper = yamlMapper; return this; } public Builder defaultMetaSchemaURI(final String defaultMetaSchemaURI) { this.defaultMetaSchemaURI = defaultMetaSchemaURI; return this; } public Builder addMetaSchema(final JsonMetaSchema jsonMetaSchema) { this.jsonMetaSchemas.put(normalizeMetaSchemaUri(jsonMetaSchema.getUri()) , jsonMetaSchema); return this; } public Builder addMetaSchemas(final Collection jsonMetaSchemas) { for (JsonMetaSchema jsonMetaSchema : jsonMetaSchemas) { addMetaSchema(jsonMetaSchema); } return this; } public Builder enableUriSchemaCache(boolean enableUriSchemaCache) { this.enableUriSchemaCache = enableUriSchemaCache; return this; } 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; } public JsonSchemaFactory build() { // create builtin keywords with (custom) formats. return new JsonSchemaFactory( jsonMapper, yamlMapper, defaultMetaSchemaURI, schemaLoadersBuilder, schemaMappersBuilder, jsonMetaSchemas, enableUriSchemaCache ); } } private final ObjectMapper jsonMapper; private final ObjectMapper yamlMapper; private final String defaultMetaSchemaURI; private final SchemaLoaders.Builder schemaLoadersBuilder; private final SchemaMappers.Builder schemaMappersBuilder; private final SchemaLoader schemaLoader; 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, final ObjectMapper yamlMapper, final String defaultMetaSchemaURI, SchemaLoaders.Builder schemaLoadersBuilder, SchemaMappers.Builder schemaMappersBuilder, final Map jsonMetaSchemas, final boolean enableUriSchemaCache) { if (defaultMetaSchemaURI == null || defaultMetaSchemaURI.trim().isEmpty()) { throw new IllegalArgumentException("defaultMetaSchemaURI must not be null or empty"); } 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"); } this.jsonMapper = jsonMapper; this.yamlMapper = yamlMapper; this.defaultMetaSchemaURI = defaultMetaSchemaURI; this.schemaLoadersBuilder = schemaLoadersBuilder; this.schemaMappersBuilder = schemaMappersBuilder; this.schemaLoader = new DefaultSchemaLoader( schemaLoadersBuilder != null ? schemaLoadersBuilder.build() : DEFAULT_SCHEMA_LOADERS, schemaMappersBuilder != null ? schemaMappersBuilder.build() : DEFAULT_SCHEMA_MAPPERS); this.jsonMetaSchemas = jsonMetaSchemas; this.enableUriSchemaCache = enableUriSchemaCache; } public SchemaLoader getSchemaLoader() { return this.schemaLoader; } /** * Builder without keywords or formats. * * Typically {@link #builder(JsonSchemaFactory)} is what is required. * * @return a builder instance without any keywords or formats - usually not what one needs. */ 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) { 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 customize 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); 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) { case V202012: return new Version202012(); case V201909: return new Version201909(); case V7: return new Version7(); case V6: return new Version6(); case V4: return new Version4(); default: throw new IllegalArgumentException("Unsupported value" + 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()) .defaultMetaSchemaURI(blueprint.defaultMetaSchemaURI) .jsonMapper(blueprint.jsonMapper) .yamlMapper(blueprint.yamlMapper); if (blueprint.schemaLoadersBuilder != null) { builder.schemaLoadersBuilder = SchemaLoaders.builder().with(blueprint.schemaLoadersBuilder); } if (blueprint.schemaMappersBuilder != null) { builder.schemaMappersBuilder = SchemaMappers.builder().with(blueprint.schemaMappersBuilder); } 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); JsonSchema jsonSchema = doCreate(validationContext, getSchemaLocation(schemaUri), new JsonNodePath(validationContext.getConfig().getPathType()), schemaNode, null, false); if (config.isPreloadJsonSchema()) { 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) { return doCreate(validationContext, schemaLocation, evaluationPath, schemaNode, parentSchema, false); } private JsonSchema doCreate(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, boolean suppressSubSchemaRetrieval) { return JsonSchema.from(withMetaSchema(validationContext, schemaNode), schemaLocation, evaluationPath, 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())) { return new ValidationContext(metaSchema, validationContext.getJsonSchemaFactory(), validationContext.getConfig(), validationContext.getSchemaReferences(), validationContext.getSchemaResources(), validationContext.getDynamicAnchors()); } return validationContext; } /** * Gets the base IRI from the schema retrieval IRI if present otherwise return * one with a null base IRI. *

* Note that the resolving of the $id or id in the schema node will take place * in the JsonSchema constructor. * * @param schemaLocation the schema retrieval uri * @return the schema location */ protected SchemaLocation getSchemaLocation(SchemaLocation schemaLocation) { return schemaLocation != null ? schemaLocation : SchemaLocation.DOCUMENT; } protected ValidationContext createValidationContext(final JsonNode schemaNode, SchemaValidatorsConfig config) { final JsonMetaSchema jsonMetaSchema = getMetaSchemaOrDefault(schemaNode, config); return new ValidationContext(jsonMetaSchema, this, config); } 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 -> getMetaSchema(id, config)); } return null; } 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()); return jsonMetaSchemas.computeIfAbsent(uri, id -> getMetaSchema(id, 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 return loadMetaSchema(id, config); }); } protected JsonMetaSchema loadMetaSchema(String id, SchemaValidatorsConfig config) { try { 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 LinkedHashMap<>()); for(Entry vocabs : vocabulary.properties()) { builder.vocabulary(vocabs.getKey(), vocabs.getValue().booleanValue()); } } } } return builder.build(); } catch (Exception e) { ValidationMessage validationMessage = ValidationMessage.builder().message("Unknown MetaSchema: {1}") .arguments(id).build(); throw new InvalidSchemaException(validationMessage, e); } } /** * 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); return newJsonSchema(null, schemaNode, config); } catch (IOException ioe) { logger.error("Failed to load json schema!", ioe); throw new JsonSchemaException(ioe); } } /** * 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, 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); return newJsonSchema(null, schemaNode, config); } catch (IOException ioe) { logger.error("Failed to load json schema!", ioe); throw new JsonSchemaException(ioe); } } /** * 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, createSchemaValidatorsConfig()); } /** * 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) { // ConcurrentHashMap computeIfAbsent does not allow calls that result in a // recursive update to the map. // The getMapperSchema potentially recurses to call back to getSchema again JsonSchema cachedUriSchema = uriSchemaCache.get(schemaUri); if (cachedUriSchema == null) { synchronized (this) { // acquire lock on shared factory object to prevent deadlock cachedUriSchema = uriSchemaCache.get(schemaUri); if (cachedUriSchema == null) { cachedUriSchema = getMappedSchema(schemaUri, config); if (cachedUriSchema != null) { uriSchemaCache.put(schemaUri, cachedUriSchema); } } } } return cachedUriSchema.withConfig(config); } 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(); } /** * Creates a schema validators config. * * @return the schema validators config */ 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) { throw new IOException("Cannot load schema at " + schemaUri.toString()); } final JsonNode schemaNode; if (isYaml(schemaUri)) { schemaNode = getYamlMapper().readTree(inputStream); } else { schemaNode = getJsonMapper().readTree(inputStream); } final JsonMetaSchema jsonMetaSchema = getMetaSchemaOrDefault(schemaNode, config); JsonNodePath evaluationPath = new JsonNodePath(config.getPathType()); if (schemaUri.getFragment() == null || schemaUri.getFragment().getNameCount() == 0) { // Schema without fragment ValidationContext validationContext = new ValidationContext(jsonMetaSchema, this, config); 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); SchemaLocation documentLocation = new SchemaLocation(schemaUri.getAbsoluteIri()); JsonSchema document = doCreate(validationContext, documentLocation, evaluationPath, schemaNode, null, false); return document.getRefSchema(schemaUri.getFragment()); } } catch (IOException 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; } } /** * 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()), 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, createSchemaValidatorsConfig()); } /** * 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, createSchemaValidatorsConfig()); } /** * 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, createSchemaValidatorsConfig()); } /** * 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, createSchemaValidatorsConfig()); } private boolean isYaml(final SchemaLocation schemaUri) { final String schemeSpecificPart = schemaUri.getAbsoluteIri().toString(); final int idx = schemeSpecificPart.lastIndexOf('.'); if (idx == -1) { // no extension; assume json return false; } final String extension = schemeSpecificPart.substring(idx); return (".yml".equals(extension) || ".yaml".equals(extension)); } /** * 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy