From 2ce955e7c3c53b16596a18b0f5f68f2b491d1de6 Mon Sep 17 00:00:00 2001 From: jhh Date: Mon, 27 Jan 2020 06:45:07 -0600 Subject: Schema validation extension to StandardCoder Issue-ID: POLICY-2331 Signed-off-by: jhh Change-Id: Ic3e8d21715d17a61ea5d07740dd9f4dd0dbb8641 Signed-off-by: jhh --- utils/pom.xml | 5 + .../policy/common/utils/coder/StandardCoder.java | 11 +- .../common/utils/coder/StandardValCoder.java | 110 ++++++++++++++++ .../common/utils/coder/StandardValCoderTest.java | 143 +++++++++++++++++++++ .../onap/policy/common/utils/coder/bad-regex.json | 9 ++ .../common/utils/coder/missing-required.json | 5 + .../policy/common/utils/coder/test.schema.json | 71 ++++++++++ .../org/onap/policy/common/utils/coder/valid.json | 9 ++ 8 files changed, 357 insertions(+), 6 deletions(-) create mode 100644 utils/src/main/java/org/onap/policy/common/utils/coder/StandardValCoder.java create mode 100644 utils/src/test/java/org/onap/policy/common/utils/coder/StandardValCoderTest.java create mode 100644 utils/src/test/resources/org/onap/policy/common/utils/coder/bad-regex.json create mode 100644 utils/src/test/resources/org/onap/policy/common/utils/coder/missing-required.json create mode 100644 utils/src/test/resources/org/onap/policy/common/utils/coder/test.schema.json create mode 100644 utils/src/test/resources/org/onap/policy/common/utils/coder/valid.json diff --git a/utils/pom.xml b/utils/pom.xml index ef059147..98afe39c 100644 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -78,6 +78,11 @@ org.eclipse.persistence javax.persistence + + com.worldturner.medeia + medeia-validator-gson + 1.1.0 + org.projectlombok lombok diff --git a/utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java b/utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java index e84a92f2..26746b79 100644 --- a/utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java +++ b/utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java @@ -1,8 +1,8 @@ /* * ============LICENSE_START======================================================= - * ONAP PAP + * ONAP * ================================================================================ - * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,7 +78,7 @@ public class StandardCoder implements Coder { try { toJson(target, object); - } catch (RuntimeException | IOException e) { + } catch (RuntimeException e) { throw new CoderException(e); } } @@ -238,9 +238,8 @@ public class StandardCoder implements Coder { * * @param target target to which to write the encoded json * @param object object to be encoded - * @throws IOException if an I/O error occurs */ - protected void toJson(Writer target, Object object) throws IOException { + protected void toJson(Writer target, Object object) { GSON.toJson(object, object.getClass(), target); } @@ -286,7 +285,7 @@ public class StandardCoder implements Coder { * @param value value to be converted * @return the converted value */ - private T convertFromDouble(Class clazz, T value) { + protected T convertFromDouble(Class clazz, T value) { if (clazz != Object.class) { return value; } diff --git a/utils/src/main/java/org/onap/policy/common/utils/coder/StandardValCoder.java b/utils/src/main/java/org/onap/policy/common/utils/coder/StandardValCoder.java new file mode 100644 index 00000000..378254b8 --- /dev/null +++ b/utils/src/main/java/org/onap/policy/common/utils/coder/StandardValCoder.java @@ -0,0 +1,110 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import com.worldturner.medeia.api.JsonSchemaVersion; +import com.worldturner.medeia.api.SchemaSource; +import com.worldturner.medeia.api.StringSchemaSource; +import com.worldturner.medeia.api.ValidationFailedException; +import com.worldturner.medeia.api.gson.MedeiaGsonApi; +import com.worldturner.medeia.schema.validation.SchemaValidator; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import lombok.NonNull; +import lombok.ToString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extension to the StandardCoder to support streaming validation + * against a Draft-07 Json schema specification. + */ + +@ToString +public class StandardValCoder extends StandardCoder { + + // The medeia-validator library integrates better than + // other libraries considered with GSON, and therefore + // the StandardCoder. + + private static final Logger logger = LoggerFactory.getLogger(StandardValCoder.class); + + private final MedeiaGsonApi validatorApi = new MedeiaGsonApi(); + private final SchemaValidator validator; + + /** + * StandardCoder with validation. + */ + public StandardValCoder(@NonNull String jsonSchema, @NonNull String name) { + SchemaSource schemaSource = + new StringSchemaSource(jsonSchema, JsonSchemaVersion.DRAFT07, null, name); + this.validator = validatorApi.loadSchema(schemaSource); + } + + @Override + protected String toJson(@NonNull Object object) { + StringWriter output = new StringWriter(); + toJson(output, object); + return String.valueOf(output); + } + + @Override + protected void toJson(@NonNull Writer target, @NonNull Object object) { + getGSON().toJson(object, object.getClass(), validatorApi.createJsonWriter(validator, target)); + } + + @Override + protected T fromJson(@NonNull Reader source, @NonNull Class clazz) { + return convertFromDouble(clazz, getGSON().fromJson(validatorApi.createJsonReader(validator, source), clazz)); + } + + @Override + protected T fromJson(String json, Class clazz) { + StringReader reader = new StringReader(json); + return convertFromDouble(clazz, getGSON().fromJson(validatorApi.createJsonReader(validator, reader), clazz)); + } + + /** + * Is the json conformant?. + */ + public boolean isConformant(@NonNull String json) { + try { + conformance(json); + } catch (CoderException e) { + logger.info("JSON is not conformant to schema {}", e); + return false; + } + return true; + } + + /** + * Check a json string for conformance against its schema definition. + */ + public void conformance(@NonNull String json) throws CoderException { + try { + validatorApi.parseAll(validatorApi.createJsonReader(validator, new StringReader(json))); + } catch (ValidationFailedException e) { + throw new CoderException(e); + } + } +} diff --git a/utils/src/test/java/org/onap/policy/common/utils/coder/StandardValCoderTest.java b/utils/src/test/java/org/onap/policy/common/utils/coder/StandardValCoderTest.java new file mode 100644 index 00000000..38106f57 --- /dev/null +++ b/utils/src/test/java/org/onap/policy/common/utils/coder/StandardValCoderTest.java @@ -0,0 +1,143 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.coder; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.worldturner.medeia.api.ValidationFailedException; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.Before; +import org.junit.Test; + +public class StandardValCoderTest { + private String jsonSchema; + private String validJson; + private String missingReqJson; + private String badRegexJson; + + @Data + @NoArgsConstructor + public static class ValOuter { + @Data + @NoArgsConstructor + public static class ValInner { + public String subItemString; + public Integer subItemInteger; + } + + public String aaString; + public int anInteger; + public boolean aaBoolean; + public List aaCollection; + } + + @Before + public void testSetUp() throws Exception { + jsonSchema = getJson("src/test/resources/org/onap/policy/common/utils/coder/test.schema.json"); + validJson = getJson("src/test/resources/org/onap/policy/common/utils/coder/valid.json"); + missingReqJson = getJson("src/test/resources/org/onap/policy/common/utils/coder/missing-required.json"); + badRegexJson = getJson("src/test/resources/org/onap/policy/common/utils/coder/bad-regex.json"); + } + + @Test + public void testDecode() throws CoderException { + StandardValCoder valCoder = new StandardValCoder(jsonSchema, "test-schema"); + + ValOuter valOuter = valCoder.decode(validJson, ValOuter.class); + assertValidJson(valOuter); + + StringReader reader = new StringReader(validJson); + valOuter = valCoder.decode(reader, ValOuter.class); + assertValidJson(valOuter); + + try { + valCoder.decode(missingReqJson, ValOuter.class); + fail("missing required field should have been flagged by the schema validation"); + } catch (CoderException e) { + assertEquals("required", ((ValidationFailedException) e.getCause()).getFailures().get(0).getRule()); + assertEquals("aaCollection", + ((ValidationFailedException) e.getCause()).getFailures().get(0).getProperty()); + assertEquals("Required property aaCollection is missing from object", + ((ValidationFailedException) e.getCause()).getFailures().get(0).getMessage()); + } + + try { + valCoder.decode(badRegexJson, ValOuter.class); + fail("bad regex should have been flagged by the schema validation"); + } catch (CoderException e) { + assertEquals("properties", ((ValidationFailedException) e.getCause()).getFailures().get(0).getRule()); + assertEquals("aaString", + ((ValidationFailedException) e.getCause()).getFailures().get(0).getProperty()); + assertEquals("Property validation failed", + ((ValidationFailedException) e.getCause()).getFailures().get(0).getMessage()); + assertEquals("pattern", + ((ValidationFailedException) e.getCause()).getFailures() + .get(0).getDetails().iterator().next().getRule()); + assertEquals("Pattern ^([a-z]*)$ is not contained in text", + ((ValidationFailedException) e.getCause()).getFailures() + .get(0).getDetails().iterator().next().getMessage()); + } + } + + @Test + public void testEncode() throws CoderException { + StandardValCoder valCoder = new StandardValCoder(jsonSchema, "test-schema"); + ValOuter valOuter = valCoder.decode(validJson, ValOuter.class); + + String valOuterJson = valCoder.encode(valOuter); + assertEquals(valOuter, valCoder.decode(valOuterJson, ValOuter.class)); + assertValidJson(valOuter); + + StringWriter writer = new StringWriter(); + valCoder.encode(writer, valOuter); + assertEquals(valOuterJson, writer.toString()); + } + + @Test + public void testConformance() { + StandardValCoder valCoder = new StandardValCoder(jsonSchema, "test-schema"); + assertTrue(valCoder.isConformant(validJson)); + assertFalse(valCoder.isConformant(missingReqJson)); + assertFalse(valCoder.isConformant(badRegexJson)); + } + + private void assertValidJson(ValOuter valOuter) { + assertEquals("abcd", valOuter.getAaString()); + assertEquals(90, valOuter.getAnInteger()); + assertTrue(valOuter.isAaBoolean()); + assertEquals("defg", valOuter.getAaCollection().get(0).getSubItemString()); + assertEquals(Integer.valueOf(1200), valOuter.getAaCollection().get(0).getSubItemInteger()); + } + + private String getJson(String filePath) throws IOException { + return new String(Files.readAllBytes(Paths.get(filePath))); + } +} \ No newline at end of file diff --git a/utils/src/test/resources/org/onap/policy/common/utils/coder/bad-regex.json b/utils/src/test/resources/org/onap/policy/common/utils/coder/bad-regex.json new file mode 100644 index 00000000..049de2cf --- /dev/null +++ b/utils/src/test/resources/org/onap/policy/common/utils/coder/bad-regex.json @@ -0,0 +1,9 @@ +{ + "aaString": "abc123", + "anInteger": 90, + "aaBoolean": true, + "aaCollection": [ { + "subItemString": "defg", + "subItemInteger": 1200 + }] +} \ No newline at end of file diff --git a/utils/src/test/resources/org/onap/policy/common/utils/coder/missing-required.json b/utils/src/test/resources/org/onap/policy/common/utils/coder/missing-required.json new file mode 100644 index 00000000..e19db9db --- /dev/null +++ b/utils/src/test/resources/org/onap/policy/common/utils/coder/missing-required.json @@ -0,0 +1,5 @@ +{ + "aaString": "abcd", + "anInteger": 90, + "aaBoolean": true +} \ No newline at end of file diff --git a/utils/src/test/resources/org/onap/policy/common/utils/coder/test.schema.json b/utils/src/test/resources/org/onap/policy/common/utils/coder/test.schema.json new file mode 100644 index 00000000..e79475eb --- /dev/null +++ b/utils/src/test/resources/org/onap/policy/common/utils/coder/test.schema.json @@ -0,0 +1,71 @@ +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://onap.org/policy/common/coders/root.json", + "type": "object", + "title": "Test Schema", + "required": [ + "aaString", + "anInteger", + "aaBoolean", + "aaCollection" + ], + "properties": { + "aaString": { + "$id": "#/properties/aaString", + "type": "string", + "title": "an alphabetical string", + "default": "", + "examples": [ + "abcdef" + ], + "pattern": "^([a-z]*)$" + }, + "anInteger": { + "$id": "#/properties/anInteger", + "type": "integer", + "title": "a bounded integer", + "default": 5, + "examples": [ + 98 + ], + "minimum": 10, + "maximum": 100 + }, + "aaBoolean": { + "$id": "#/properties/aaBoolean", + "type": "boolean", + "title": "a boolean", + "default": false, + "examples": [ + true + ] + }, + "aaCollection": { + "$id": "#/properties/aaCollection", + "type": "array", + "title": "a collection", + "items": { + "$id": "#/properties/aaCollection/items", + "type": "object", + "title": "the collection items", + "required": [ + "subItemString" + ], + "properties": { + "subItemString": { + "$id": "#/properties/aaCollection/items/properties/subItemString", + "type": "string", + "title": "the subitem string", + "default": "blah", + "pattern": "^(.*)$" + }, + "subItemInteger": { + "$id": "#/properties/aaCollection/items/properties/subItemInteger", + "type": "integer" + } + } + } + } + } +} \ No newline at end of file diff --git a/utils/src/test/resources/org/onap/policy/common/utils/coder/valid.json b/utils/src/test/resources/org/onap/policy/common/utils/coder/valid.json new file mode 100644 index 00000000..c1738176 --- /dev/null +++ b/utils/src/test/resources/org/onap/policy/common/utils/coder/valid.json @@ -0,0 +1,9 @@ +{ + "aaString": "abcd", + "anInteger": 90, + "aaBoolean": true, + "aaCollection": [ { + "subItemString": "defg", + "subItemInteger": 1200 + }] +} \ No newline at end of file -- cgit 1.2.3-korg