Skip to content

Commit 577b647

Browse files
fduttonFaron Duttonfdutton
authored
Adds support for translating one URI into another (#682)
* Adds support for translating one URI into another * Adds documentation about URITranslator. --------- Co-authored-by: Faron Dutton <[email protected]> Co-authored-by: fdutton <fdutton@noreply>
1 parent 2d9d5ef commit 577b647

File tree

5 files changed

+308
-21
lines changed

5 files changed

+308
-21
lines changed

doc/schema-map.md

Lines changed: 125 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,141 @@
11
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.
22

33
* Some applications are running inside a corporate network without Internet access.
4-
* Some of the Internet resources are not reliable
4+
* Some of the Internet resources are not reliable.
5+
* A test environment may serve unpublished schemas, which are not yet available at the location identified by the payload's `$schema` property.
56

67
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.
78

89
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.
910

1011
Note that when using a mapping, the local copy is always used, and the external reference is not queried.
1112

12-
### Usage
13+
### URI Translation
1314

14-
Basically, you can specify a mapping in the builder. For more details, please take a look at the test cases and the [PR](https://github.com/networknt/json-schema-validator/pull/125).
15+
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.
1516

17+
#### Examples
1618

17-
### Real Example
19+
Automatically map HTTP to HTTPS
1820

19-
https://github.com/JMRI/JMRI/blob/master/java/src/jmri/server/json/schema-map.json
21+
```java
22+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
23+
config.addUriTranlator(uri -> {
24+
if ("http".equalsIgnoreCase(uri.getScheme()) {
25+
try {
26+
return new URI(
27+
"https",
28+
uri.getUserInfo(),
29+
uri.getHost(),
30+
uri.getPort(),
31+
uri.getPath(),
32+
uri.getQuery(),
33+
uri.getFragment()
34+
);
35+
} catch (URISyntaxException x) {
36+
throw new IllegalArgumentException(x.getMessage(), x);
37+
}
38+
}
39+
return uri;
40+
});
41+
```
2042

21-
In case you provide the schema through an `InputStream` or a `String` to resolve `$ref` with URN (relative path), you need to provide the `URNFactory` to the `JsonSchemaFactory.Builder.
22-
URNFactory` interface will allow you to resolve URN to URI.
43+
Map a public schema to a test environment
44+
45+
```java
46+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
47+
config.addUriTranlator(uri -> {
48+
if (true
49+
&& "https".equalsIgnoreCase(uri.getScheme()
50+
&& "schemas.acme.org".equalsIgnoreCase(uri.getHost())
51+
&& (-1 == uri.getPort() || 443 == uri.getPort())
52+
) {
53+
try {
54+
return new URI(
55+
"http",
56+
uri.getUserInfo(),
57+
"test-schemas.acme.org",
58+
8080,
59+
uri.getPath(),
60+
uri.getQuery(),
61+
uri.getFragment()
62+
);
63+
} catch (URISyntaxException x) {
64+
throw new IllegalArgumentException(x.getMessage(), x);
65+
}
66+
}
67+
return uri;
68+
});
69+
```
70+
71+
Replace a URI with another
72+
73+
**Note:**
74+
This also works for mapping URNs to resources.
75+
76+
```java
77+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
78+
config.addUriTranlator(URITranslator.map("https://schemas.acme.org/Foo", "classpath://Foo");
79+
```
80+
81+
### Precedence
82+
83+
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:
84+
85+
```
86+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
87+
config.addUriTranlator(uri -> {
88+
if ("http".equalsIgnoreCase(uri.getScheme()) {
89+
try {
90+
return new URI(
91+
"https",
92+
uri.getUserInfo(),
93+
uri.getHost(),
94+
uri.getPort(),
95+
uri.getPath(),
96+
uri.getQuery(),
97+
uri.getFragment()
98+
);
99+
} catch (URISyntaxException x) {
100+
throw new IllegalArgumentException(x.getMessage(), x);
101+
}
102+
}
103+
return uri;
104+
});
105+
config.addUriTranlator(uri -> {
106+
if (true
107+
&& "https".equalsIgnoreCase(uri.getScheme()
108+
&& "schemas.acme.org".equalsIgnoreCase(uri.getHost())
109+
&& (-1 == uri.getPort() || 443 == uri.getPort())
110+
) {
111+
try {
112+
return new URI(
113+
"http",
114+
uri.getUserInfo(),
115+
"test-schemas.acme.org",
116+
8080,
117+
uri.getPath(),
118+
uri.getQuery(),
119+
uri.getFragment()
120+
);
121+
} catch (URISyntaxException x) {
122+
throw new IllegalArgumentException(x.getMessage(), x);
123+
}
124+
}
125+
return uri;
126+
});
127+
config.addUriTranlator(URITranslator.map("http://test-schemas.acme.org:8080/Foo", "classpath://Foo");
128+
```
129+
130+
Given a starting URI of `https://schemas.acme.org/Foo`, the configuration above produces the following translations (in order):
131+
132+
1. The translation from HTTP to HTTPS does not occur since the original URI already specifies HTTPS.
133+
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.
134+
3. The third rule receives the URI produced by the second rule and performs a simple mapping to a local resource.
135+
136+
Since all `JsonSchemaFactory`s are created from an optional `SchemaValidatorsConfig`, any `URITranslator`s added to the factory are evaluated after those provided by `SchemaValidatorsConfig`.
137+
138+
### Deprecated
139+
140+
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.
23141

24-
please take a look at the test cases and the [PR](https://github.com/networknt/json-schema-validator/pull/274).

src/main/java/com/networknt/schema/JsonSchemaFactory.java

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.fasterxml.jackson.databind.ObjectMapper;
2121
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
2222
import com.networknt.schema.uri.*;
23+
import com.networknt.schema.uri.URITranslator.CompositeURITranslator;
2324
import com.networknt.schema.urn.URNFactory;
2425
import org.slf4j.Logger;
2526
import org.slf4j.LoggerFactory;
@@ -52,6 +53,7 @@ public static class Builder {
5253
private boolean forceHttps = true;
5354
private boolean removeEmptyFragmentSuffix = true;
5455
private boolean enableUriSchemaCache = true;
56+
private final CompositeURITranslator uriTranslators = new CompositeURITranslator();
5557

5658
public Builder() {
5759
// Adds support for creating {@link URL}s.
@@ -138,11 +140,22 @@ public Builder addMetaSchemas(final Collection<? extends JsonMetaSchema> jsonMet
138140
return this;
139141
}
140142

143+
/**
144+
* @deprecated Use {@code addUriTranslator} instead.
145+
*/
146+
@Deprecated
141147
public Builder addUriMappings(final Map<String, String> map) {
142148
this.uriMap.putAll(map);
143149
return this;
144150
}
145151

152+
public Builder addUriTranslator(URITranslator translator) {
153+
if (null != translator) {
154+
this.uriTranslators.add(translator);
155+
}
156+
return this;
157+
}
158+
146159
public Builder addUrnFactory(URNFactory urnFactory) {
147160
this.urnFactory = urnFactory;
148161
return this;
@@ -176,7 +189,8 @@ public JsonSchemaFactory build() {
176189
uriMap,
177190
forceHttps,
178191
removeEmptyFragmentSuffix,
179-
enableUriSchemaCache
192+
enableUriSchemaCache,
193+
uriTranslators
180194
);
181195
}
182196
}
@@ -186,6 +200,7 @@ public JsonSchemaFactory build() {
186200
private final String defaultMetaSchemaURI;
187201
private final URISchemeFactory uriFactory;
188202
private final URISchemeFetcher uriFetcher;
203+
private final CompositeURITranslator uriTranslators;
189204
private final URNFactory urnFactory;
190205
private final Map<String, JsonMetaSchema> jsonMetaSchemas;
191206
private final Map<String, String> uriMap;
@@ -206,7 +221,8 @@ private JsonSchemaFactory(
206221
final Map<String, String> uriMap,
207222
final boolean forceHttps,
208223
final boolean removeEmptyFragmentSuffix,
209-
final boolean enableUriSchemaCache) {
224+
final boolean enableUriSchemaCache,
225+
final CompositeURITranslator uriTranslators) {
210226
if (jsonMapper == null) {
211227
throw new IllegalArgumentException("ObjectMapper must not be null");
212228
} else if (yamlMapper == null) {
@@ -223,6 +239,8 @@ private JsonSchemaFactory(
223239
throw new IllegalArgumentException("Meta Schema for default Meta Schema URI must be provided");
224240
} else if (uriMap == null) {
225241
throw new IllegalArgumentException("URL Mappings must not be null");
242+
} else if (uriTranslators == null) {
243+
throw new IllegalArgumentException("URI Translators must not be null");
226244
}
227245
this.jsonMapper = jsonMapper;
228246
this.yamlMapper = yamlMapper;
@@ -235,6 +253,7 @@ private JsonSchemaFactory(
235253
this.forceHttps = forceHttps;
236254
this.removeEmptyFragmentSuffix = removeEmptyFragmentSuffix;
237255
this.enableUriSchemaCache = enableUriSchemaCache;
256+
this.uriTranslators = uriTranslators;
238257
}
239258

240259
/**
@@ -301,6 +320,9 @@ public static Builder builder(final JsonSchemaFactory blueprint) {
301320
.yamlMapper(blueprint.yamlMapper)
302321
.addUriMappings(blueprint.uriMap);
303322

323+
for (URITranslator translator: blueprint.uriTranslators) {
324+
builder = builder.addUriTranslator(translator);
325+
}
304326
for (Map.Entry<String, URIFactory> entry : blueprint.uriFactory.getURIFactories().entrySet()) {
305327
builder = builder.uriFactory(entry.getValue(), entry.getKey());
306328
}
@@ -341,6 +363,10 @@ public URIFactory getUriFactory() {
341363
return this.uriFactory;
342364
}
343365

366+
public URITranslator getUriTranslator() {
367+
return this.uriTranslators.with(URITranslator.map(uriMap));
368+
}
369+
344370
public JsonSchema getSchema(final String schema, final SchemaValidatorsConfig config) {
345371
try {
346372
final JsonNode schemaNode = jsonMapper.readTree(schema);
@@ -372,12 +398,11 @@ public JsonSchema getSchema(final InputStream schemaStream) {
372398
public JsonSchema getSchema(final URI schemaUri, final SchemaValidatorsConfig config) {
373399
try {
374400
InputStream inputStream = null;
375-
final Map<String, String> map = (config != null) ? config.getUriMappings() : new HashMap<String, String>();
376-
map.putAll(uriMap);
401+
final URITranslator uriTranslator = null == config ? getUriTranslator() : config.getUriTranslator().with(getUriTranslator());
377402

378403
final URI mappedUri;
379404
try {
380-
mappedUri = this.uriFactory.create(map.get(schemaUri.toString()) != null ? map.get(schemaUri.toString()) : schemaUri.toString());
405+
mappedUri = this.uriFactory.create(uriTranslator.translate(schemaUri).toString());
381406
} catch (IllegalArgumentException e) {
382407
logger.error("Failed to create URI.", e);
383408
throw new JsonSchemaException(e);

src/main/java/com/networknt/schema/SchemaValidatorsConfig.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.Set;
2424

2525
import com.fasterxml.jackson.databind.JsonNode;
26+
import com.networknt.schema.uri.URITranslator;
27+
import com.networknt.schema.uri.URITranslator.CompositeURITranslator;
2628
import com.networknt.schema.walk.JsonSchemaWalkListener;
2729

2830
public class SchemaValidatorsConfig {
@@ -70,6 +72,8 @@ public class SchemaValidatorsConfig {
7072
*/
7173
private Map<String, String> uriMappings = new HashMap<String, String>();
7274

75+
private CompositeURITranslator uriTranslators = new CompositeURITranslator();
76+
7377
/**
7478
* When a field is set as nullable in the OpenAPI specification, the schema validator validates that it is nullable
7579
* however continues with validation against the nullable field
@@ -130,11 +134,30 @@ public ApplyDefaultsStrategy getApplyDefaultsStrategy() {
130134
return applyDefaultsStrategy;
131135
}
132136

137+
public CompositeURITranslator getUriTranslator() {
138+
return this.uriTranslators
139+
.with(URITranslator.map(this.uriMappings));
140+
}
141+
142+
public void addUriTranslator(URITranslator uriTranslator) {
143+
if (null != uriTranslator) {
144+
this.uriTranslators.add(uriTranslator);
145+
}
146+
}
147+
148+
/**
149+
* @deprecated Use {@code getUriTranslator()} instead
150+
*/
151+
@Deprecated
133152
public Map<String, String> getUriMappings() {
134153
// return a copy of the mappings
135154
return new HashMap<String, String>(uriMappings);
136155
}
137156

157+
/**
158+
* @deprecated Use {@code addUriTranslator()} instead
159+
*/
160+
@Deprecated
138161
public void setUriMappings(Map<String, String> uriMappings) {
139162
this.uriMappings = uriMappings;
140163
}

0 commit comments

Comments
 (0)