Skip to content

Commit

Permalink
feat: Add URL mappings
Browse files Browse the repository at this point in the history
URL mappings allows public, normally internet accessible URLs (such as might be externally referenced in a schema) to be mapped to local URLs that might be on the filesystem, at a different external URL, or embedded within a JAR file.

This allows the JsonSchema validator to validate a schema with an external reference to validate against the local copy of that reference when the external reference is not available. Note that when using a mapping, the local copy is always used, and the external reference is not queried.
  • Loading branch information
rhwood committed Mar 22, 2019
1 parent 4d6df4f commit 22efdfe
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 4 deletions.
89 changes: 85 additions & 4 deletions src/main/java/com/networknt/schema/JsonSchemaFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
Expand All @@ -29,6 +30,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.url.StandardURLFetcher;
import com.networknt.schema.url.URLFactory;
import com.networknt.schema.url.URLFetcher;

public class JsonSchemaFactory {
Expand All @@ -41,6 +43,7 @@ public static class Builder {
private URLFetcher urlFetcher;
private String defaultMetaSchemaURI;
private Map<String, JsonMetaSchema> jsonMetaSchemas = new HashMap<String, JsonMetaSchema>();
private Map<URL, URL> urlMap = new HashMap<URL, URL>();

public Builder objectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
Expand Down Expand Up @@ -69,13 +72,35 @@ public Builder addMetaSchemas(Collection<? extends JsonMetaSchema> jsonMetaSchem
return this;
}

public Builder addUrlMappings(URL url) throws MalformedURLException, IOException {
if (objectMapper == null) {
objectMapper = new ObjectMapper();
}
return addUrlMappings(objectMapper.readTree(url));
}

public Builder addUrlMappings(JsonNode jsonNode) throws MalformedURLException {
HashMap<URL, URL> map = new HashMap<URL, URL>();
for (JsonNode mapping : jsonNode) {
map.put(URLFactory.toURL(mapping.get("publicURL").asText()),
URLFactory.toURL(mapping.get("localURL").asText()));
}
return addUrlMappings(map);
}

public Builder addUrlMappings(Map<URL, URL> map) {
this.urlMap.putAll(map);
return this;
}

public JsonSchemaFactory build() {
// create builtin keywords with (custom) formats.
return new JsonSchemaFactory(
objectMapper == null ? new ObjectMapper() : objectMapper,
urlFetcher == null ? new StandardURLFetcher(): urlFetcher,
defaultMetaSchemaURI,
jsonMetaSchemas
jsonMetaSchemas,
urlMap
);
}
}
Expand All @@ -84,8 +109,9 @@ public JsonSchemaFactory build() {
private final URLFetcher urlFetcher;
private final String defaultMetaSchemaURI;
private final Map<String, JsonMetaSchema> jsonMetaSchemas;
private final Map<URL, URL> urlMap;

private JsonSchemaFactory(ObjectMapper mapper, URLFetcher urlFetcher, String defaultMetaSchemaURI, Map<String, JsonMetaSchema> jsonMetaSchemas) {
private JsonSchemaFactory(ObjectMapper mapper, URLFetcher urlFetcher, String defaultMetaSchemaURI, Map<String, JsonMetaSchema> jsonMetaSchemas, Map<URL, URL> urlMap) {
if (mapper == null) {
throw new IllegalArgumentException("ObjectMapper must not be null");
}
Expand All @@ -101,10 +127,14 @@ private JsonSchemaFactory(ObjectMapper mapper, URLFetcher urlFetcher, String def
if (jsonMetaSchemas.get(defaultMetaSchemaURI) == null) {
throw new IllegalArgumentException("Meta Schema for default Meta Schema URI must be provided");
}
if (urlMap == null) {
throw new IllegalArgumentException("URL Mappings must not be null");
}
this.mapper = mapper;
this.defaultMetaSchemaURI = defaultMetaSchemaURI;
this.urlFetcher = urlFetcher;
this.jsonMetaSchemas = jsonMetaSchemas;
this.urlMap = urlMap;
}

/**
Expand Down Expand Up @@ -135,7 +165,8 @@ public static Builder builder(JsonSchemaFactory blueprint) {
.addMetaSchemas(blueprint.jsonMetaSchemas.values())
.urlFetcher(blueprint.urlFetcher)
.defaultMetaSchemaURI(blueprint.defaultMetaSchemaURI)
.objectMapper(blueprint.mapper);
.objectMapper(blueprint.mapper)
.addUrlMappings(blueprint.urlMap);
}

private JsonSchema newJsonSchema(JsonNode schemaNode, SchemaValidatorsConfig config) {
Expand Down Expand Up @@ -191,8 +222,9 @@ public JsonSchema getSchema(InputStream schemaStream) {
public JsonSchema getSchema(URL schemaURL, SchemaValidatorsConfig config) {
try {
InputStream inputStream = null;
URL mappedURL = urlMap.getOrDefault(schemaURL, schemaURL);
try {
inputStream = urlFetcher.fetch(schemaURL);
inputStream = urlFetcher.fetch(mappedURL);
JsonNode schemaNode = mapper.readTree(inputStream);
final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode);

Expand Down Expand Up @@ -225,6 +257,55 @@ public JsonSchema getSchema(JsonNode jsonNode) {
return newJsonSchema(jsonNode, null);
}

/**
* Add URL mappings contained in a given URL.
*
* @param url resource containing URL mappings in a JSON array
* @throws IOException if unable to parse urlMappings
* @see #addUrlMappings(JsonNode)
*/
public JsonSchemaFactory addUrlMappings(URL url) throws IOException {
return addUrlMappings(mapper.readTree(url));
}

/**
* Add URL mappings containined in a given JSON array.
*
* An example array is: <code>
* [
* {
* "publicURL": "http://json-schema.org/draft-04/schema#",
* "localURL": "resource:/draftv4.schema.json"
* },
* {
* "publicURL": "https://raw.githubusercontent.com/networknt/json-schema-validator/master/src/main/resources/url-mapping.schema.json",
* "localURL": "resource:/com/networknt/schema/url-mapping.schema.json"
* }
* ]
* </code>
*
* @param jsonNode JSON array containing URL mappings
* @throws MalformedURLException if any URL mapping is malformed
* @see #addUrlMappings(Map)
*/
public JsonSchemaFactory addUrlMappings(JsonNode jsonNode) throws MalformedURLException {
HashMap<URL, URL> map = new HashMap<URL, URL>();
for (JsonNode mapping : jsonNode) {
map.put(URLFactory.toURL(mapping.get("publicURL").asText()), URLFactory.toURL(mapping.get("localURL").asText()));
}
return addUrlMappings(map);
}

/**
* Add URL mappings containined in a given map.
*
* @param map Map of URL mappings, where the public URL is the key, and the local URL to use is the value
*/
public JsonSchemaFactory addUrlMappings(Map<URL, URL> map) {
urlMap.putAll(map);
return this;
}

private boolean idMatchesSourceUrl(JsonMetaSchema metaSchema, JsonNode schema, URL schemaUrl) {

String id = metaSchema.readId(schema);
Expand Down
25 changes: 25 additions & 0 deletions src/main/resources/com/networknt/schema/url-mapping.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "json-schema-validator-url-mapping",
"type": "array",
"description": "Data portion of message from JMRI to client for type \"node\"",
"items": {
"type": "object",
"uniqueItems": true,
"properties": {
"publicURL": {
"type": "string",
"description": "Public, presumably internet-accessible, URL for schema"
},
"localURL": {
"type": "string",
"description": "Local URL for schema that will be used when a schema references the public URL"
}
},
"additionalProperties": false,
"required": [
"publicURL",
"localURL"
]
}
}
67 changes: 67 additions & 0 deletions src/test/java/com/networknt/schema/UrlMappingTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.networknt.schema;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.net.UnknownHostException;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.url.URLFactory;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

public class UrlMappingTest {

private final ObjectMapper mapper = new ObjectMapper();

/**
* Validate that a JSON URL Mapping file containing the URL Mapping schema
* is schema valid.
*
* @throws IOException if unable to parse the mapping file
*/
@Test
public void testUrlMappingUrl() throws IOException {
JsonSchemaFactory instance = JsonSchemaFactory.getInstance();
URL mappings = URLFactory.toURL("resource:tests/url_mapping/url-mapping.json");
instance.addUrlMappings(mappings);
JsonSchema schema = instance.getSchema(new URL("https://raw.githubusercontent.com/networknt/json-schema-validator/master/src/main/resources/url-mapping.schema.json"));
assertEquals(0, schema.validate(mapper.readTree(mappings)).size());
}

/**
* Validate that local URL is used when attempting to get a schema that is not
* available publicly. Use the URL http://example.com/invalid/schema/url to use
* a public URL that returns a 404 Not Found. The locally mapped schema is a
* valid, but empty schema.
*
* @throws IOException if unable to parse the mapping file
*/
@Test
public void testExampleMappings() throws IOException {
JsonSchemaFactory instance = JsonSchemaFactory.getInstance();
URL example = new URL("http://example.com/invalid/schema/url");
// first test that attempting to use example URL throws an error
try {
JsonSchema schema = instance.getSchema(example);
schema.validate(mapper.createObjectNode());
fail("Expected exception not thrown");
} catch (JsonSchemaException ex) {
Throwable cause = ex.getCause();
if (!(cause instanceof FileNotFoundException ||
cause instanceof UnknownHostException)) {
fail("Unexpected cause for JsonSchemaException");
}
// passing, so do nothing
} catch (Exception ex) {
fail("Unexpected exception thrown");
}
URL mappings = URLFactory.toURL("resource:tests/url_mapping/invalid-schema-url.json");
instance.addUrlMappings(mappings);
JsonSchema schema = instance.getSchema(example);
assertEquals(0, schema.validate(mapper.createObjectNode()).size());
}
}
3 changes: 3 additions & 0 deletions src/test/resources/tests/url_mapping/example-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"$schema": "http://json-schema.org/draft-04/schema#"
}
10 changes: 10 additions & 0 deletions src/test/resources/tests/url_mapping/invalid-schema-url.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"publicURL": "http://json-schema.org/draft-04/schema#",
"localURL": "resource:/draftv4.schema.json"
},
{
"publicURL": "http://example.com/invalid/schema/url",
"localURL": "resource:/tests/url_mapping/example-schema.json"
}
]
10 changes: 10 additions & 0 deletions src/test/resources/tests/url_mapping/url-mapping.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"publicURL": "http://json-schema.org/draft-04/schema#",
"localURL": "resource:/draftv4.schema.json"
},
{
"publicURL": "https://raw.githubusercontent.com/networknt/json-schema-validator/master/src/main/resources/url-mapping.schema.json",
"localURL": "resource:/com/networknt/schema/url-mapping.schema.json"
}
]

0 comments on commit 22efdfe

Please sign in to comment.