| /* |
| * 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.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; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| |
| public class JsonSchemaFactory { |
| private static final Logger logger = LoggerFactory |
| .getLogger(JsonSchemaFactory.class); |
| |
| |
| public static class Builder { |
| private ObjectMapper objectMapper = new ObjectMapper(); |
| private YAMLMapper yamlMapper = new YAMLMapper(); |
| private String defaultMetaSchemaURI; |
| private final Map<String, URIFactory> uriFactoryMap = new HashMap<String, URIFactory>(); |
| private final Map<String, URIFetcher> uriFetcherMap = new HashMap<String, URIFetcher>(); |
| private URNFactory urnFactory; |
| private final ConcurrentMap<String, JsonMetaSchema> jsonMetaSchemas = new ConcurrentHashMap<String, JsonMetaSchema>(); |
| private final Map<String, String> uriMap = new HashMap<String, String>(); |
| 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); |
| } |
| } |
| |
| public Builder objectMapper(final ObjectMapper objectMapper) { |
| this.objectMapper = objectMapper; |
| return this; |
| } |
| |
| public Builder yamlMapper(final YAMLMapper yamlMapper) { |
| this.yamlMapper = yamlMapper; |
| return this; |
| } |
| |
| public Builder defaultMetaSchemaURI(final String defaultMetaSchemaURI) { |
| this.defaultMetaSchemaURI = 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<String> 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<String> 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; |
| } |
| |
| public Builder addMetaSchemas(final Collection<? extends JsonMetaSchema> jsonMetaSchemas) { |
| for (JsonMetaSchema jsonMetaSchema : jsonMetaSchemas) { |
| addMetaSchema(jsonMetaSchema); |
| } |
| return this; |
| } |
| |
| /** |
| * @deprecated Use {@code addUriTranslator} instead. |
| * @param map the map of uri mappings. |
| * @return this builder. |
| */ |
| @Deprecated |
| public Builder addUriMappings(final Map<String, String> 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) { |
| return this; |
| } |
| |
| /** |
| * @deprecated No longer necessary. |
| * @param removeEmptyFragmentSuffix ignored. |
| * @return this builder. |
| */ |
| public Builder removeEmptyFragmentSuffix(boolean removeEmptyFragmentSuffix) { |
| return this; |
| } |
| |
| public Builder enableUriSchemaCache(boolean enableUriSchemaCache) { |
| this.enableUriSchemaCache = enableUriSchemaCache; |
| return this; |
| } |
| |
| public JsonSchemaFactory build() { |
| // create builtin keywords with (custom) formats. |
| return new JsonSchemaFactory( |
| objectMapper == null ? new ObjectMapper() : objectMapper, |
| yamlMapper == null ? new YAMLMapper(): yamlMapper, |
| defaultMetaSchemaURI, |
| new URISchemeFactory(uriFactoryMap), |
| new URISchemeFetcher(uriFetcherMap), |
| urnFactory, |
| jsonMetaSchemas, |
| uriMap, |
| enableUriSchemaCache, |
| uriTranslators |
| ); |
| } |
| } |
| |
| 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 Map<String, JsonMetaSchema> jsonMetaSchemas; |
| private final Map<String, String> uriMap; |
| private final ConcurrentMap<URI, JsonSchema> uriSchemaCache = new ConcurrentHashMap<URI, JsonSchema>(); |
| private final boolean enableUriSchemaCache; |
| |
| |
| private JsonSchemaFactory( |
| final ObjectMapper jsonMapper, |
| final YAMLMapper yamlMapper, |
| final String defaultMetaSchemaURI, |
| final URISchemeFactory uriFactory, |
| final URISchemeFetcher uriFetcher, |
| final URNFactory urnFactory, |
| final Map<String, JsonMetaSchema> jsonMetaSchemas, |
| final Map<String, String> uriMap, |
| final boolean enableUriSchemaCache, |
| final CompositeURITranslator uriTranslators) { |
| 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 (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.jsonMetaSchemas = jsonMetaSchemas; |
| this.uriMap = uriMap; |
| this.enableUriSchemaCache = enableUriSchemaCache; |
| this.uriTranslators = uriTranslators; |
| } |
| |
| /** |
| * Builder without keywords or formats. |
| * |
| * <code> |
| * JsonSchemaFactory.builder(JsonSchemaFactory.getDraftV4()).build(); |
| * </code> |
| * |
| * @return a builder instance without any keywords or formats - usually not what one needs. |
| */ |
| 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(); |
| return builder() |
| .defaultMetaSchemaURI(metaSchema.getUri()) |
| .addMetaSchema(metaSchema) |
| .build(); |
| } |
| |
| 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); |
| } |
| } |
| |
| public static Builder builder(final JsonSchemaFactory blueprint) { |
| Builder builder = builder() |
| .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<String, URIFactory> entry : blueprint.uriFactory.getURIFactories().entrySet()) { |
| builder = builder.uriFactory(entry.getValue(), entry.getKey()); |
| } |
| for (Map.Entry<String, URIFetcher> entry : blueprint.uriFetcher.getURIFetchers().entrySet()) { |
| builder = builder.uriFetcher(entry.getValue(), entry.getKey()); |
| } |
| return builder; |
| } |
| |
| protected JsonSchema newJsonSchema(final URI schemaUri, final JsonNode schemaNode, final SchemaValidatorsConfig config) { |
| final ValidationContext validationContext = createValidationContext(schemaNode); |
| validationContext.setConfig(config); |
| return doCreate(validationContext, "#", schemaUri, schemaNode, null, false); |
| } |
| |
| public JsonSchema create(ValidationContext validationContext, String schemaPath, JsonNode schemaNode, JsonSchema parentSchema) { |
| return doCreate(validationContext, null == schemaPath ? "#" : schemaPath, parentSchema.getCurrentUri(), schemaNode, parentSchema, false); |
| } |
| |
| private JsonSchema doCreate(ValidationContext validationContext, String schemaPath, URI currentUri, JsonNode schemaNode, JsonSchema parentSchema, boolean suppressSubSchemaRetrieval) { |
| return JsonSchema.from(validationContext, schemaPath, currentUri, schemaNode, parentSchema, suppressSubSchemaRetrieval); |
| } |
| |
| protected ValidationContext createValidationContext(final JsonNode schemaNode) { |
| final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); |
| return new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, null); |
| } |
| |
| private JsonMetaSchema findMetaSchemaForSchema(final JsonNode schemaNode) { |
| 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); |
| return jsonMetaSchema; |
| } |
| |
| private JsonMetaSchema fromId(String id) { |
| // Is it a well-known dialect? |
| return SpecVersionDetector.detectOptionalVersion(id) |
| .map(JsonSchemaFactory::checkVersion) |
| .map(JsonSchemaVersion::getInstance) |
| .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); |
| return newJsonSchema(null, schemaNode, config); |
| } catch (IOException ioe) { |
| logger.error("Failed to load json schema!", ioe); |
| throw new JsonSchemaException(ioe); |
| } |
| } |
| |
| public JsonSchema getSchema(final String schema) { |
| return getSchema(schema, null); |
| } |
| |
| public JsonSchema getSchema(final InputStream schemaStream, final SchemaValidatorsConfig config) { |
| try { |
| final JsonNode schemaNode = jsonMapper.readTree(schemaStream); |
| return newJsonSchema(null, schemaNode, config); |
| } catch (IOException ioe) { |
| logger.error("Failed to load json schema!", ioe); |
| throw new JsonSchemaException(ioe); |
| } |
| } |
| |
| public JsonSchema getSchema(final InputStream schemaStream) { |
| return getSchema(schemaStream, null); |
| } |
| |
| public JsonSchema getSchema(final URI schemaUri, final SchemaValidatorsConfig config) { |
| try { |
| InputStream inputStream = null; |
| 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); |
| } |
| |
| if (enableUriSchemaCache && uriSchemaCache.containsKey(mappedUri)) { |
| JsonSchema cachedUriSchema = uriSchemaCache.get(mappedUri); |
| // 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. |
| cachedUriSchema.getValidationContext().setConfig(config); |
| return cachedUriSchema; |
| } |
| |
| try { |
| inputStream = this.uriFetcher.fetch(mappedUri); |
| |
| final JsonNode schemaNode; |
| if (isYaml(mappedUri)) { |
| schemaNode = yamlMapper.readTree(inputStream); |
| } else { |
| schemaNode = jsonMapper.readTree(inputStream); |
| } |
| |
| final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); |
| |
| JsonSchema jsonSchema; |
| if (idMatchesSourceUri(jsonMetaSchema, schemaNode, schemaUri)) { |
| ValidationContext validationContext = new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, config); |
| jsonSchema = doCreate(validationContext, "#", mappedUri, schemaNode, null, true /* retrieved via id, resolving will not change anything */); |
| } else { |
| final ValidationContext validationContext = createValidationContext(schemaNode); |
| validationContext.setConfig(config); |
| jsonSchema = doCreate(validationContext, "#", mappedUri, schemaNode, null, false); |
| } |
| |
| if (enableUriSchemaCache) { |
| uriSchemaCache.put(mappedUri, jsonSchema); |
| } |
| |
| return jsonSchema; |
| } finally { |
| if (inputStream != null) { |
| inputStream.close(); |
| } |
| } |
| } catch (IOException ioe) { |
| logger.error("Failed to load json schema!", ioe); |
| throw new JsonSchemaException(ioe); |
| } |
| } |
| |
| public JsonSchema getSchema(final URI schemaUri) { |
| return getSchema(schemaUri, new SchemaValidatorsConfig()); |
| } |
| |
| public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode, final SchemaValidatorsConfig config) { |
| return newJsonSchema(schemaUri, jsonNode, config); |
| } |
| |
| |
| public JsonSchema getSchema(final JsonNode jsonNode, final SchemaValidatorsConfig config) { |
| return newJsonSchema(null, jsonNode, config); |
| } |
| |
| public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode) { |
| return newJsonSchema(schemaUri, jsonNode, null); |
| } |
| |
| public JsonSchema getSchema(final JsonNode jsonNode) { |
| return newJsonSchema(null, jsonNode, null); |
| } |
| |
| private boolean idMatchesSourceUri(final JsonMetaSchema metaSchema, final JsonNode schema, final URI 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 URI schemaUri) { |
| final String schemeSpecificPart = schemaUri.getSchemeSpecificPart(); |
| 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)); |
| } |
| |
| 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); |
| } |
| } |
| } |