Skip to content

Commit 318e5e9

Browse files
committed
Add JSON schema validator originally developed in OERSI (#443)
1 parent 2fa964a commit 318e5e9

File tree

6 files changed

+309
-0
lines changed

6 files changed

+309
-0
lines changed

metafacture-json/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependencies {
2222
implementation 'com.fasterxml.jackson.core:jackson-core:2.13.0'
2323
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
2424
implementation 'com.jayway.jsonpath:json-path:2.6.0'
25+
implementation 'com.github.erosb:everit-json-schema:1.14.1'
2526
testImplementation 'junit:junit:4.12'
2627
testImplementation 'org.mockito:mockito-core:2.5.5'
2728
testRuntimeOnly 'org.slf4j:slf4j-simple:1.7.21'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright 2021, 2022 Fabian Steeg, hbz
3+
*
4+
* Licensed under the Apache License, Version 2.0 the "License";
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.metafacture.json;
18+
19+
import org.metafacture.framework.FluxCommand;
20+
import org.metafacture.framework.MetafactureException;
21+
import org.metafacture.framework.ObjectReceiver;
22+
import org.metafacture.framework.annotations.Description;
23+
import org.metafacture.framework.annotations.In;
24+
import org.metafacture.framework.annotations.Out;
25+
import org.metafacture.framework.helpers.DefaultObjectPipe;
26+
27+
import org.everit.json.schema.Schema;
28+
import org.everit.json.schema.ValidationException;
29+
import org.everit.json.schema.loader.SchemaClient;
30+
import org.everit.json.schema.loader.SchemaLoader;
31+
import org.json.JSONException;
32+
import org.json.JSONObject;
33+
import org.json.JSONTokener;
34+
import org.slf4j.Logger;
35+
import org.slf4j.LoggerFactory;
36+
37+
import java.io.FileWriter;
38+
import java.io.IOException;
39+
import java.io.InputStream;
40+
41+
/**
42+
* Validate JSON against a given schema, pass only valid input to the receiver.
43+
*
44+
* @author Fabian Steeg (fsteeg)
45+
*/
46+
@Description("Validate JSON against a given schema, send only valid input to the receiver. Pass the schema location to validate against. " +
47+
"Set 'schemaRoot' for resolving sub-schemas referenced in '$id' or '$ref' (defaults to the classpath root: '/'). " +
48+
"Write valid and/or invalid output to locations specified with 'writeValid' and 'writeInvalid'.")
49+
@In(String.class)
50+
@Out(String.class)
51+
@FluxCommand("validate-json")
52+
public final class JsonValidator extends DefaultObjectPipe<String, ObjectReceiver<String>> {
53+
54+
private static final Logger LOG = LoggerFactory.getLogger(JsonValidator.class);
55+
private static final String DEFAULT_SCHEMA_ROOT = "/";
56+
private String schemaUrl;
57+
private Schema schema;
58+
private long fail;
59+
private long success;
60+
private FileWriter writeInvalid;
61+
private FileWriter writeValid;
62+
private String schemaRoot = DEFAULT_SCHEMA_ROOT;
63+
64+
/**
65+
* @param url The URL of the schema to validate against.
66+
*/
67+
public JsonValidator(final String url) {
68+
this.schemaUrl = url;
69+
}
70+
71+
/**
72+
* @param schemaRoot The root location for resolving sub-schemas referenced in '$id' or '$ref'.
73+
*/
74+
public void setSchemaRoot(final String schemaRoot) {
75+
this.schemaRoot = schemaRoot;
76+
}
77+
78+
/**
79+
* @param writeValid The location to write valid data to.
80+
*/
81+
public void setWriteValid(final String writeValid) {
82+
this.writeValid = fileWriter(writeValid);
83+
}
84+
85+
/**
86+
* @param writeInvalid The location to write invalid data to.
87+
*/
88+
public void setWriteInvalid(final String writeInvalid) {
89+
this.writeInvalid = fileWriter(writeInvalid);
90+
}
91+
92+
@Override
93+
public void process(final String json) {
94+
JSONObject object = null;
95+
try {
96+
object = new JSONObject(json); // throws JSONException on syntax error
97+
}
98+
catch (final JSONException e) {
99+
handleInvalid(json, null, e.getMessage());
100+
}
101+
try {
102+
initSchema();
103+
schema.validate(object); // throws ValidationException if invalid
104+
getReceiver().process(json);
105+
++success;
106+
write(json, writeValid);
107+
}
108+
catch (final ValidationException e) {
109+
handleInvalid(json, object, e.getAllMessages().toString());
110+
}
111+
}
112+
113+
@Override
114+
protected void onCloseStream() {
115+
close(writeInvalid);
116+
close(writeValid);
117+
LOG.debug("Success: {}, Fail: {}", success, fail);
118+
super.onCloseStream();
119+
}
120+
121+
private void initSchema() {
122+
if (schema != null) {
123+
return;
124+
}
125+
try (InputStream inputStream = getClass().getResourceAsStream(schemaUrl)) {
126+
schema = SchemaLoader.builder()
127+
.schemaJson(new JSONObject(new JSONTokener(inputStream)))
128+
.schemaClient(SchemaClient.classPathAwareClient())
129+
.resolutionScope("classpath://" + schemaRoot)
130+
.build().load().build();
131+
}
132+
catch (final IOException | JSONException e) {
133+
throw new MetafactureException(e.getMessage(), e);
134+
}
135+
}
136+
137+
private FileWriter fileWriter(final String fileLocation) {
138+
try {
139+
return new FileWriter(fileLocation);
140+
}
141+
catch (final IOException e) {
142+
throw new MetafactureException(e.getMessage(), e);
143+
}
144+
}
145+
146+
private void handleInvalid(final String json, final JSONObject object,
147+
final String errorMessage) {
148+
LOG.info("Invalid JSON: {} in {}", errorMessage, object != null ? object.opt("id") : json);
149+
++fail;
150+
write(json, writeInvalid);
151+
}
152+
153+
private void write(final String json, final FileWriter fileWriter) {
154+
if (fileWriter != null) {
155+
try {
156+
fileWriter.append(json);
157+
fileWriter.append("\n");
158+
}
159+
catch (final IOException e) {
160+
throw new MetafactureException(e.getMessage(), e);
161+
}
162+
}
163+
}
164+
165+
private void close(final FileWriter fileWriter) {
166+
if (fileWriter != null) {
167+
try {
168+
fileWriter.close();
169+
}
170+
catch (final IOException e) {
171+
throw new MetafactureException(e.getMessage(), e);
172+
}
173+
}
174+
}
175+
176+
}

metafacture-json/src/main/resources/flux-commands.properties

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
#
1616
encode-json org.metafacture.json.JsonEncoder
1717
decode-json org.metafacture.json.JsonDecoder
18+
validate-json org.metafacture.json.JsonValidator
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2021, 2022 Fabian Steeg, hbz
3+
*
4+
* Licensed under the Apache License, Version 2.0 the "License";
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.metafacture.json;
17+
18+
import org.junit.After;
19+
import org.junit.Before;
20+
import org.junit.Test;
21+
import org.metafacture.framework.MetafactureException;
22+
import org.metafacture.framework.ObjectReceiver;
23+
import org.mockito.InOrder;
24+
import org.mockito.Mock;
25+
import org.mockito.Mockito;
26+
import org.mockito.MockitoAnnotations;
27+
28+
/**
29+
* Tests for {@link JsonValidator}.
30+
*
31+
* @author Fabian Steeg
32+
*
33+
*/
34+
public final class JsonValidatorTest {
35+
36+
private static final String SCHEMA = "/schemas/schema.json";
37+
private static final String JSON_VALID = "{\"id\":\"http://example.org/\"}";
38+
private static final String JSON_INVALID_MISSING_REQUIRED = "{}";
39+
private static final String JSON_INVALID_URI_FORMAT= "{\"id\":\"example.org/\"}";
40+
private static final String JSON_INVALID_DUPLICATE_KEY = "{\"id\":\"val\",\"id\":\"val\"}";
41+
private static final String JSON_INVALID_SYNTAX_ERROR = "{\"id1\":\"val\",\"id2\":\"val\"";
42+
43+
private JsonValidator validator;
44+
45+
@Mock
46+
private ObjectReceiver<String> receiver;
47+
private InOrder inOrder;
48+
49+
@Before
50+
public void setup() {
51+
MockitoAnnotations.initMocks(this);
52+
validator = new JsonValidator(SCHEMA);
53+
validator.setSchemaRoot("/schemas/");
54+
validator.setReceiver(receiver);
55+
inOrder = Mockito.inOrder(receiver);
56+
}
57+
58+
@Test
59+
public void testShouldValidate() {
60+
validator.process(JSON_VALID);
61+
inOrder.verify(receiver, Mockito.calls(1)).process(JSON_VALID);
62+
}
63+
64+
@Test
65+
public void testShouldInvalidateMissingRequired() {
66+
validator.process(JSON_INVALID_MISSING_REQUIRED);
67+
inOrder.verifyNoMoreInteractions();
68+
}
69+
70+
@Test
71+
public void testShouldInvalidateUriFormat() {
72+
validator.process(JSON_INVALID_URI_FORMAT);
73+
inOrder.verifyNoMoreInteractions();
74+
}
75+
76+
@Test
77+
public void testShouldInvalidateDuplicateKey() {
78+
validator.process(JSON_INVALID_DUPLICATE_KEY);
79+
inOrder.verifyNoMoreInteractions();
80+
}
81+
82+
@Test
83+
public void testShouldInvalidateSyntaxError() {
84+
validator.process(JSON_INVALID_SYNTAX_ERROR);
85+
inOrder.verifyNoMoreInteractions();
86+
}
87+
88+
@Test(expected = MetafactureException.class)
89+
public void testShouldCatchMissingSchemaFile() {
90+
new JsonValidator("").process("");
91+
}
92+
93+
@Test(expected = MetafactureException.class)
94+
public void testShouldCatchMissingValidOutputFile() {
95+
validator.setWriteValid("");
96+
validator.process(JSON_INVALID_MISSING_REQUIRED);
97+
}
98+
99+
@Test(expected = MetafactureException.class)
100+
public void testShouldCatchMissingInvalidOutputFile() {
101+
validator.setWriteInvalid("");
102+
validator.process(JSON_INVALID_MISSING_REQUIRED);
103+
}
104+
105+
@After
106+
public void cleanup() {
107+
validator.closeStream();
108+
}
109+
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"$id": "id.json",
3+
"$schema": "http://json-schema.org/draft-07/schema#",
4+
"title": "URL",
5+
"description": "The URL/URI of the resource",
6+
"type": "string",
7+
"format": "uri"
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$id": "schema.json",
4+
"type": "object",
5+
"properties": {
6+
"id": {
7+
"$ref": "id.json"
8+
}
9+
},
10+
"required": [
11+
"id"
12+
]
13+
}

0 commit comments

Comments
 (0)