Schema validation extension to StandardCoder

Issue-ID: POLICY-2331
Signed-off-by: jhh <jorge.hernandez-herrero@att.com>
Change-Id: Ic3e8d21715d17a61ea5d07740dd9f4dd0dbb8641
Signed-off-by: jhh <jorge.hernandez-herrero@att.com>
diff --git a/utils/pom.xml b/utils/pom.xml
index ef05914..98afe39 100644
--- a/utils/pom.xml
+++ b/utils/pom.xml
@@ -79,6 +79,11 @@
             <artifactId>javax.persistence</artifactId>
         </dependency>
         <dependency>
+            <groupId>com.worldturner.medeia</groupId>
+            <artifactId>medeia-validator-gson</artifactId>
+            <version>1.1.0</version>
+        </dependency>
+        <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
             <scope>provided</scope>
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 e84a92f..26746b7 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 @@
         try {
             toJson(target, object);
 
-        } catch (RuntimeException | IOException e) {
+        } catch (RuntimeException e) {
             throw new CoderException(e);
         }
     }
@@ -238,9 +238,8 @@
      *
      * @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 @@
      * @param value value to be converted
      * @return the converted value
      */
-    private <T> T convertFromDouble(Class<T> clazz, T value) {
+    protected <T> T convertFromDouble(Class<T> 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 0000000..378254b
--- /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> T fromJson(@NonNull Reader source, @NonNull Class<T> clazz) {
+        return convertFromDouble(clazz, getGSON().fromJson(validatorApi.createJsonReader(validator, source), clazz));
+    }
+
+    @Override
+    protected <T> T fromJson(String json, Class<T> 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 0000000..38106f5
--- /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<ValInner> 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 0000000..049de2c
--- /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 0000000..e19db9d
--- /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 0000000..e79475e
--- /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 0000000..c173817
--- /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