Extract YamlJsonTranslator from StandardYamlCoder 82/95682/2
authorJim Hahn <jrh3@att.com>
Fri, 13 Sep 2019 16:14:25 +0000 (12:14 -0400)
committerJim Hahn <jrh3@att.com>
Fri, 13 Sep 2019 21:40:29 +0000 (17:40 -0400)
Refactored StandardYamlCoder, extracting a new class, YamlJsonTranslator,
from it.  This facilitates performing yaml translation when not using a
standard "gson" coder.
Added YamlJacksonHandler which supports YAML translation layered on
top of a JacksonHandler instead of a GsonMessageBodyHandler.
Also added junit tests to complete coverage of StandardCoder.
Also added public APPLICATION_YAML to YamlMessageBodyHandler.

Change-Id: Ia470fa194661fbf1aebeaf0f18b57f2a984cb64b
Issue-ID: POLICY-2081
Signed-off-by: Jim Hahn <jrh3@att.com>
17 files changed:
gson/src/main/java/org/onap/policy/common/gson/GsonMessageBodyHandler.java
gson/src/main/java/org/onap/policy/common/gson/JacksonHandler.java
policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/server/YamlExceptionMapper.java
policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/server/YamlJacksonHandler.java [new file with mode: 0644]
policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/server/YamlMessageBodyHandler.java
policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/HttpServerTest.java
policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/RestEchoService.java
policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/RestServerTest.java
policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/YamlJacksonHandlerTest.java [new file with mode: 0644]
policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/YamlMessageBodyHandlerTest.java
utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java
utils/src/main/java/org/onap/policy/common/utils/coder/StandardYamlCoder.java
utils/src/main/java/org/onap/policy/common/utils/coder/YamlJsonTranslator.java [new file with mode: 0644]
utils/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java
utils/src/test/java/org/onap/policy/common/utils/coder/StandardYamlCoderTest.java
utils/src/test/java/org/onap/policy/common/utils/coder/YamlJsonTranslatorTest.java [new file with mode: 0644]
utils/src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml [moved from utils/src/test/resources/org/onap/policy/common/utils/coder/StandardYamlCoder.yaml with 100% similarity]

index a36f8a0..ad27091 100644 (file)
@@ -62,9 +62,17 @@ public class GsonMessageBodyHandler implements MessageBodyReader<Object>, Messag
      * into Integer/Long, where possible.
      */
     public GsonMessageBodyHandler() {
-        this(new GsonBuilder().registerTypeAdapterFactory(new MapDoubleAdapterFactory()).create());
+        this(new GsonBuilder());
+    }
 
-        logger.info("Using GSON for REST calls");
+    /**
+     * Constructs the object, using a Gson object that translates Doubles inside of Maps
+     * into Integer/Long, where possible.
+     *
+     * @param builder builder to use to create the gson object
+     */
+    public GsonMessageBodyHandler(GsonBuilder builder) {
+        this(builder.registerTypeAdapterFactory(new MapDoubleAdapterFactory()).create());
     }
 
     /**
@@ -74,6 +82,8 @@ public class GsonMessageBodyHandler implements MessageBodyReader<Object>, Messag
      */
     public GsonMessageBodyHandler(Gson gson) {
         this.gson = gson;
+
+        logger.info("Using GSON for REST calls");
     }
 
     @Override
index ad9692f..6d94643 100644 (file)
@@ -49,9 +49,7 @@ public class JacksonHandler extends GsonMessageBodyHandler {
         super(builder
                         .registerTypeAdapterFactory(new JacksonFieldAdapterFactory())
                         .registerTypeAdapterFactory(new JacksonMethodAdapterFactory())
-                        .registerTypeAdapterFactory(new MapDoubleAdapterFactory())
-                        .setExclusionStrategies(new JacksonExclusionStrategy())
-                        .create());
+                        .setExclusionStrategies(new JacksonExclusionStrategy()));
     }
 
 }
index ac96cab..7eac932 100644 (file)
@@ -34,7 +34,7 @@ import org.yaml.snakeyaml.error.YAMLException;
  * error code to an HTTP 400 error code.
  */
 @Provider
-@Produces("application/yaml")
+@Produces(YamlMessageBodyHandler.APPLICATION_YAML)
 public class YamlExceptionMapper implements ExceptionMapper<YAMLException> {
     private static Logger logger = LoggerFactory.getLogger(YamlExceptionMapper.class);
 
diff --git a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/server/YamlJacksonHandler.java b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/http/server/YamlJacksonHandler.java
new file mode 100644 (file)
index 0000000..0cab374
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.endpoints.http.server;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.ext.Provider;
+import org.onap.policy.common.gson.JacksonHandler;
+import org.onap.policy.common.utils.coder.YamlJsonTranslator;
+
+/**
+ * Provider that serializes and de-serializes YAML via snakeyaml and gson using jackson
+ * default behaviors and annotations.
+ */
+@Provider
+@Consumes(MediaType.WILDCARD)
+@Produces(MediaType.WILDCARD)
+public class YamlJacksonHandler extends YamlMessageBodyHandler {
+
+    /**
+     * Translator to be used. We want a GSON object that's configured the same way as it
+     * is in {@link JacksonHandler}, so just get it from there.
+     */
+    private static final YamlJsonTranslator TRANS = new YamlJsonTranslator(new JacksonHandler().getGson());
+
+    /**
+     * Constructs the object.
+     */
+    public YamlJacksonHandler() {
+        super(TRANS);
+    }
+}
index 36418e4..89aa8ff 100644 (file)
@@ -26,7 +26,6 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
-import java.io.Reader;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 import java.nio.charset.StandardCharsets;
@@ -37,14 +36,14 @@ import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.ext.MessageBodyReader;
 import javax.ws.rs.ext.MessageBodyWriter;
 import javax.ws.rs.ext.Provider;
-import org.onap.policy.common.utils.coder.CoderException;
-import org.onap.policy.common.utils.coder.StandardYamlCoder;
+import org.onap.policy.common.gson.GsonMessageBodyHandler;
+import org.onap.policy.common.utils.coder.YamlJsonTranslator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.yaml.snakeyaml.error.YAMLException;
 
 /**
- * Provider that serializes and de-serializes JSON via gson.
+ * Provider that serializes and de-serializes YAML via snakeyaml and gson.
  */
 @Provider
 @Consumes(MediaType.WILDCARD)
@@ -53,11 +52,33 @@ public class YamlMessageBodyHandler implements MessageBodyReader<Object>, Messag
 
     public static final Logger logger = LoggerFactory.getLogger(YamlMessageBodyHandler.class);
 
+    public static final String APPLICATION_YAML = "application/yaml";
+
+    /**
+     * Translator that's used when none is specified. We want a GSON object that's
+     * configured the same way as it is in {@link GsonMessageBodyHandler}, so just get it
+     * from there.
+     */
+    private static final YamlJsonTranslator DEFAULT_TRANSLATOR =
+                    new YamlJsonTranslator(new GsonMessageBodyHandler().getGson());
+
+    private final YamlJsonTranslator translator;
+
     /**
      * Constructs the object.
      */
     public YamlMessageBodyHandler() {
+        this(DEFAULT_TRANSLATOR);
+    }
+
+    /**
+     * Constructs the object.
+     *
+     * @param translator translator to use to translate to/from YAML
+     */
+    public YamlMessageBodyHandler(YamlJsonTranslator translator) {
         logger.info("Accepting YAML for REST calls");
+        this.translator = translator;
     }
 
     @Override
@@ -75,10 +96,7 @@ public class YamlMessageBodyHandler implements MessageBodyReader<Object>, Messag
                     MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException {
 
         try (OutputStreamWriter writer = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)) {
-            new MyYamlCoder().encode(writer, object);
-
-        } catch (CoderException e) {
-            throw new IOException(e);
+            translator.toYaml(writer, object);
         }
     }
 
@@ -104,24 +122,10 @@ public class YamlMessageBodyHandler implements MessageBodyReader<Object>, Messag
 
         try (InputStreamReader streamReader = new InputStreamReader(entityStream, StandardCharsets.UTF_8)) {
             Class<?> clazz = (Class<?>) genericType;
-            return new MyYamlCoder().decode(streamReader, clazz);
-        }
-    }
+            return translator.fromYaml(streamReader, clazz);
 
-    /**
-     * Yaml coder that yields YAMLException on input so that the http servlet can identify
-     * it and generate a bad-request status code. Only the {@link #decode(Reader, Class)}
-     * method must be overridden.
-     */
-    private static class MyYamlCoder extends StandardYamlCoder {
-        @Override
-        public <T> T decode(Reader source, Class<T> clazz) {
-            try {
-                return fromJson(source, clazz);
-
-            } catch (JsonSyntaxException e) {
-                throw new YAMLException(e);
-            }
+        } catch (JsonSyntaxException e) {
+            throw new YAMLException(e);
         }
     }
 }
index 84f82f1..6dee6f1 100644 (file)
@@ -39,6 +39,7 @@ import org.junit.Before;
 import org.junit.Test;
 import org.onap.policy.common.endpoints.http.server.HttpServletServer;
 import org.onap.policy.common.endpoints.http.server.HttpServletServerFactoryInstance;
+import org.onap.policy.common.endpoints.http.server.YamlMessageBodyHandler;
 import org.onap.policy.common.utils.coder.StandardYamlCoder;
 import org.onap.policy.common.utils.gson.GsonTestUtils;
 import org.onap.policy.common.utils.network.NetworkUtil;
@@ -51,7 +52,7 @@ import org.slf4j.LoggerFactory;
 public class HttpServerTest {
     private static final String LOCALHOST = "localhost";
     private static final String JSON_MEDIA = "application/json";
-    private static final String YAML_MEDIA = "application/yaml";
+    private static final String YAML_MEDIA = YamlMessageBodyHandler.APPLICATION_YAML;
     private static final String SWAGGER_JSON = "/swagger.json";
     private static final String JUNIT_ECHO_HELLO = "/junit/echo/hello";
     private static final String JUNIT_ECHO_FULL_REQUEST = "/junit/echo/full/request";
index 27ce300..373950b 100644 (file)
@@ -32,6 +32,7 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
+import org.onap.policy.common.endpoints.http.server.YamlMessageBodyHandler;
 
 
 @Api(value = "echo")
@@ -56,7 +57,7 @@ public class RestEchoService {
 
     @POST
     @Path("/full/request")
-    @Produces({MediaType.APPLICATION_JSON, "application/yaml"})
+    @Produces({MediaType.APPLICATION_JSON, YamlMessageBodyHandler.APPLICATION_YAML})
     @ApiOperation(value = "echoes back the request structure", response = RestEchoReqResp.class)
     public Response echoFullyPost(RestEchoReqResp reqResp) {
         return Response.status(Status.OK).entity(reqResp).build();
index cd40f01..519bbd1 100644 (file)
@@ -69,7 +69,6 @@ import org.onap.policy.common.utils.network.NetworkUtil;
 import org.powermock.reflect.Whitebox;
 
 public class RestServerTest {
-    private static final String APPLICATION_YAML = "application/yaml";
     private static final String SERVER1 = "my-server-A";
     private static final String SERVER2 = "my-server-B";
     private static final String FACTORY_FIELD = "factory";
@@ -247,8 +246,9 @@ public class RestServerTest {
     public void testInvalidYaml() throws Exception {
         initRealParams();
 
-        assertEquals(200, roundTrip(new StandardCoder().encode(new MyRequest()), APPLICATION_YAML));
-        assertEquals(400, roundTrip("<bogus yaml", APPLICATION_YAML));
+        assertEquals(200, roundTrip(new StandardCoder().encode(new MyRequest()),
+                        YamlMessageBodyHandler.APPLICATION_YAML));
+        assertEquals(400, roundTrip("<bogus yaml", YamlMessageBodyHandler.APPLICATION_YAML));
         assertThat(errorMsg).contains("Invalid request");
     }
 
@@ -340,8 +340,8 @@ public class RestServerTest {
     }
 
     @Path("/")
-    @Produces({MediaType.APPLICATION_JSON, APPLICATION_YAML})
-    @Consumes({MediaType.APPLICATION_JSON, APPLICATION_YAML})
+    @Produces({MediaType.APPLICATION_JSON, YamlMessageBodyHandler.APPLICATION_YAML})
+    @Consumes({MediaType.APPLICATION_JSON, YamlMessageBodyHandler.APPLICATION_YAML})
     public static class RealProvider {
         @POST
         @Path("/request")
diff --git a/policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/YamlJacksonHandlerTest.java b/policy-endpoints/src/test/java/org/onap/policy/common/endpoints/http/server/test/YamlJacksonHandlerTest.java
new file mode 100644 (file)
index 0000000..7ca131e
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.endpoints.http.server.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gson.JsonObject;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+import javax.ws.rs.core.MediaType;
+import org.junit.Test;
+import org.onap.policy.common.endpoints.http.server.YamlJacksonHandler;
+import org.onap.policy.common.endpoints.http.server.YamlMessageBodyHandler;
+import org.onap.policy.common.gson.annotation.GsonJsonAnyGetter;
+import org.onap.policy.common.gson.annotation.GsonJsonAnySetter;
+
+public class YamlJacksonHandlerTest {
+
+    @Test
+    public void test() throws Exception {
+        YamlJacksonHandler hdlr = new YamlJacksonHandler();
+
+        assertTrue(hdlr.isReadable(null, null, null, MediaType.valueOf(YamlMessageBodyHandler.APPLICATION_YAML)));
+        assertFalse(hdlr.isReadable(null, null, null, MediaType.TEXT_PLAIN_TYPE));
+
+        JsonObject expected = new JsonObject();
+        expected.addProperty("myId", 100);
+        expected.addProperty("value", "a value");
+        expected.addProperty("abc", "def");
+        expected.addProperty("hello", "world");
+
+        Data data = new Data();
+        data.id = 10;
+        data.value = "a value";
+        data.props = new HashMap<>();
+        data.props.put("abc", "def");
+        data.props.put("hello", "world");
+
+        /*
+         * Ensure everything serializes as expected.
+         */
+        ByteArrayOutputStream outstr = new ByteArrayOutputStream();
+        hdlr.writeTo(data, Data.class, Data.class, null, null, null, outstr);
+
+        assertEquals("abc: def\nhello: world\nmyId: 100\nvalue: a value\n", outstr.toString("UTF-8"));
+
+        /*
+         * Ensure everything deserializes as expected.
+         */
+        Data data2 = (Data) hdlr.readFrom(Object.class, Data.class, null, null, null,
+                        new ByteArrayInputStream(outstr.toByteArray()));
+
+        // id is not serialized, so we must copy it manually before comparing
+        data2.id = data.id;
+
+        assertEquals(data.toString(), data2.toString());
+    }
+
+    @Test
+    public void testMapDouble() throws Exception {
+        MyMap map = new MyMap();
+        map.props = new HashMap<>();
+        map.props.put("plainString", "def");
+        map.props.put("negInt", -10);
+        map.props.put("doubleVal", 12.5);
+        map.props.put("posLong", 100000000000L);
+
+        YamlJacksonHandler hdlr = new YamlJacksonHandler();
+        ByteArrayOutputStream outstr = new ByteArrayOutputStream();
+        hdlr.writeTo(map, map.getClass(), map.getClass(), null, null, null, outstr);
+
+        Object obj2 = hdlr.readFrom(Object.class, map.getClass(), null, null, null,
+                        new ByteArrayInputStream(outstr.toByteArray()));
+        assertEquals(map.toString(), obj2.toString());
+
+        map = (MyMap) obj2;
+
+        assertEquals(-10, map.props.get("negInt"));
+        assertEquals(100000000000L, map.props.get("posLong"));
+        assertEquals(12.5, map.props.get("doubleVal"));
+    }
+
+    /**
+     * This class includes all policy-specific gson annotations.
+     */
+    public static class Data {
+        protected int id;
+
+        protected String value;
+
+        protected Map<String, String> props;
+
+        public int getMyId() {
+            return 100;
+        }
+
+        public String getValue() {
+            return value;
+        }
+
+        public void setValue(String value) {
+            this.value = value;
+        }
+
+        @GsonJsonAnyGetter
+        public Map<String, String> getProps() {
+            return props;
+        }
+
+        /**
+         * Sets a property.
+         *
+         * @param name property name
+         * @param value new value
+         */
+        @GsonJsonAnySetter
+        public void setProperty(String name, String value) {
+            if (props == null) {
+                props = new TreeMap<>();
+            }
+
+            props.put(name, value);
+        }
+
+        @Override
+        public String toString() {
+            return "Data [id=" + id + ", value=" + value + ", props=" + props + "]";
+        }
+    }
+
+    private static class MyMap {
+        private Map<String, Object> props;
+
+        @Override
+        public String toString() {
+            return props.toString();
+        }
+
+        @SuppressWarnings("unused")
+        public Map<String, Object> getProps() {
+            return props;
+        }
+
+        @SuppressWarnings("unused")
+        public void setProps(Map<String, Object> props) {
+            this.props = props;
+        }
+    }
+}
index 0ddad0b..70b620c 100644 (file)
@@ -117,7 +117,7 @@ public class YamlMessageBodyHandlerTest {
         MyObject obj1 = new MyObject(10);
 
         assertThatThrownBy(() -> hdlr.writeTo(obj1, obj1.getClass(), CLASS_OBJ, null, null, null, outstr))
-                        .isInstanceOf(IOException.class);
+                        .isInstanceOf(YAMLException.class);
 
         outstr.close();
     }
index d3d6981..c3f2d99 100644 (file)
@@ -38,6 +38,8 @@ import java.io.OutputStreamWriter;
 import java.io.Reader;
 import java.io.Writer;
 import java.nio.charset.StandardCharsets;
+import lombok.AccessLevel;
+import lombok.Getter;
 import org.onap.policy.common.gson.DoubleConverter;
 import org.onap.policy.common.gson.MapDoubleAdapterFactory;
 
@@ -49,6 +51,7 @@ public class StandardCoder implements Coder {
     /**
      * Gson object used to encode and decode messages.
      */
+    @Getter(AccessLevel.PROTECTED)
     private static final Gson GSON =
                     new GsonBuilder().registerTypeAdapter(StandardCoderObject.class, new StandardTypeAdapter())
                                     .registerTypeAdapterFactory(new MapDoubleAdapterFactory()).create();
index 357b106..36f15b9 100644 (file)
 
 package org.onap.policy.common.utils.coder;
 
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonNull;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonPrimitive;
-import java.io.IOException;
 import java.io.Reader;
-import java.io.StringReader;
-import java.io.StringWriter;
 import java.io.Writer;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map.Entry;
-import org.yaml.snakeyaml.DumperOptions;
-import org.yaml.snakeyaml.Yaml;
-import org.yaml.snakeyaml.emitter.Emitter;
-import org.yaml.snakeyaml.error.YAMLException;
-import org.yaml.snakeyaml.nodes.MappingNode;
-import org.yaml.snakeyaml.nodes.Node;
-import org.yaml.snakeyaml.nodes.NodeTuple;
-import org.yaml.snakeyaml.nodes.ScalarNode;
-import org.yaml.snakeyaml.nodes.SequenceNode;
-import org.yaml.snakeyaml.nodes.Tag;
-import org.yaml.snakeyaml.resolver.Resolver;
-import org.yaml.snakeyaml.serializer.Serializer;
 
 /**
  * YAML encoder and decoder using the "standard" mechanism, which is currently gson. All
  * of the methods perform conversion to/from YAML (instead of JSON).
  */
 public class StandardYamlCoder extends StandardCoder {
+    private final YamlJsonTranslator translator;
 
     /**
      * Constructs the object.
      */
     public StandardYamlCoder() {
-        super();
+        translator = new YamlJsonTranslator(getGSON());
     }
 
     @Override
     protected String toJson(Object object) {
-        StringWriter output = new StringWriter();
-        toJson(output, object);
-        return output.toString();
+        return translator.toYaml(object);
     }
 
     @Override
     protected void toJson(Writer target, Object object) {
-        DumperOptions dumper = new DumperOptions();
-        Serializer serializer = new Serializer(new Emitter(target, dumper), new Resolver(), dumper, null);
-
-        try {
-            serializer.open();
-            serializer.serialize(makeYaml(toJsonTree(object)));
-            serializer.close();
-
-        } catch (IOException e) {
-            throw new YAMLException(e);
-        }
+        translator.toYaml(target, object);
     }
 
     @Override
     protected <T> T fromJson(String yaml, Class<T> clazz) {
-        Node node = new Yaml().compose(new StringReader(yaml));
-        return fromJson(makeGson(node), clazz);
+        return translator.fromYaml(yaml, clazz);
     }
 
     @Override
     protected <T> T fromJson(Reader source, Class<T> clazz) {
-        Node node = new Yaml().compose(source);
-        return fromJson(makeGson(node), clazz);
-    }
-
-    /**
-     * Converts an arbitrary gson element into a corresponding Yaml node.
-     *
-     * @param jel gson element to be converted
-     * @return a yaml node corresponding to the element
-     */
-    private Node makeYaml(JsonElement jel) {
-        if (jel.isJsonArray()) {
-            return makeYamlSequence((JsonArray) jel);
-
-        } else if (jel.isJsonObject()) {
-            return makeYamlMap((JsonObject) jel);
-
-        } else if (jel.isJsonPrimitive()) {
-            return makeYamlPrim((JsonPrimitive) jel);
-
-        } else {
-            return new ScalarNode(Tag.NULL, "", null, null, DumperOptions.ScalarStyle.PLAIN);
-        }
-    }
-
-    /**
-     * Converts an arbitrary gson array into a corresponding Yaml sequence.
-     *
-     * @param jel gson element to be converted
-     * @return a yaml node corresponding to the element
-     */
-    private Node makeYamlSequence(JsonArray jel) {
-        List<Node> nodes = new ArrayList<>(jel.size());
-        jel.forEach(item -> nodes.add(makeYaml(item)));
-
-        return new SequenceNode(Tag.SEQ, true, nodes, null, null, DumperOptions.FlowStyle.AUTO);
-    }
-
-    /**
-     * Converts an arbitrary gson object into a corresponding Yaml map.
-     *
-     * @param jel gson element to be converted
-     * @return a yaml node corresponding to the element
-     */
-    private Node makeYamlMap(JsonObject jel) {
-        List<NodeTuple> nodes = new ArrayList<>(jel.size());
-
-        for (Entry<String, JsonElement> entry : jel.entrySet()) {
-            Node key = new ScalarNode(Tag.STR, entry.getKey(), null, null, DumperOptions.ScalarStyle.PLAIN);
-            Node value = makeYaml(entry.getValue());
-
-            nodes.add(new NodeTuple(key, value));
-        }
-
-        return new MappingNode(Tag.MAP, true, nodes, null, null, DumperOptions.FlowStyle.AUTO);
-    }
-
-    /**
-     * Converts an arbitrary gson primitive into a corresponding Yaml scalar.
-     *
-     * @param jel gson element to be converted
-     * @return a yaml node corresponding to the element
-     */
-    private Node makeYamlPrim(JsonPrimitive jel) {
-        Tag tag;
-        if (jel.isNumber()) {
-            Class<? extends Number> clazz = jel.getAsNumber().getClass();
-
-            if (clazz == Double.class || clazz == Float.class) {
-                tag = Tag.FLOAT;
-
-            } else {
-                tag = Tag.INT;
-            }
-
-        } else if (jel.isBoolean()) {
-            tag = Tag.BOOL;
-
-        } else {
-            // treat anything else as a string
-            tag = Tag.STR;
-        }
-
-        return new ScalarNode(tag, jel.getAsString(), null, null, DumperOptions.ScalarStyle.PLAIN);
-    }
-
-    /**
-     * Converts an arbitrary Yaml node into a corresponding gson element.
-     *
-     * @param node node to be converted
-     * @return a gson element corresponding to the node
-     */
-    private JsonElement makeGson(Node node) {
-        if (node instanceof MappingNode) {
-            return makeGsonObject((MappingNode) node);
-
-        } else if (node instanceof SequenceNode) {
-            return makeGsonArray((SequenceNode) node);
-
-        } else {
-            return makeGsonPrim((ScalarNode) node);
-        }
-
-        // yaml doesn't appear to use anchor nodes when decoding so ignore them for now
-    }
-
-    /**
-     * Converts a Yaml sequence into a corresponding gson array.
-     *
-     * @param node node to be converted
-     * @return a gson element corresponding to the node
-     */
-    private JsonElement makeGsonArray(SequenceNode node) {
-        List<Node> nodes = node.getValue();
-
-        JsonArray array = new JsonArray(nodes.size());
-        nodes.forEach(subnode -> array.add(makeGson(subnode)));
-
-        return array;
-    }
-
-    /**
-     * Converts a Yaml map into a corresponding gson object.
-     *
-     * @param node node to be converted
-     * @return a gson element corresponding to the node
-     */
-    private JsonElement makeGsonObject(MappingNode node) {
-        JsonObject obj = new JsonObject();
-
-        for (NodeTuple tuple : node.getValue()) {
-            Node key = tuple.getKeyNode();
-            String skey = ((ScalarNode) key).getValue();
-
-            obj.add(skey, makeGson(tuple.getValueNode()));
-        }
-
-        return obj;
-    }
-
-    /**
-     * Converts a Yaml scalar into a corresponding gson primitive.
-     *
-     * @param node node to be converted
-     * @return a gson element corresponding to the node
-     */
-    private JsonElement makeGsonPrim(ScalarNode node) {
-        try {
-            Tag tag = node.getTag();
-
-            if (tag == Tag.INT) {
-                return new JsonPrimitive(Long.valueOf(node.getValue()));
-
-            } else if (tag == Tag.FLOAT) {
-                return new JsonPrimitive(Double.valueOf(node.getValue()));
-
-            } else if (tag == Tag.BOOL) {
-                return new JsonPrimitive(Boolean.valueOf(node.getValue()));
-
-            } else if (tag == Tag.NULL) {
-                return JsonNull.INSTANCE;
-
-            } else {
-                // treat anything else as a string
-                return new JsonPrimitive(node.getValue());
-            }
-
-        } catch (NumberFormatException ex) {
-            // just treat it as a string
-            return new JsonPrimitive(node.getValue());
-        }
+        return translator.fromYaml(source, clazz);
     }
 }
diff --git a/utils/src/main/java/org/onap/policy/common/utils/coder/YamlJsonTranslator.java b/utils/src/main/java/org/onap/policy/common/utils/coder/YamlJsonTranslator.java
new file mode 100644 (file)
index 0000000..906c9fd
--- /dev/null
@@ -0,0 +1,321 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.coder;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.emitter.Emitter;
+import org.yaml.snakeyaml.error.YAMLException;
+import org.yaml.snakeyaml.nodes.MappingNode;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.NodeTuple;
+import org.yaml.snakeyaml.nodes.ScalarNode;
+import org.yaml.snakeyaml.nodes.SequenceNode;
+import org.yaml.snakeyaml.nodes.Tag;
+import org.yaml.snakeyaml.resolver.Resolver;
+import org.yaml.snakeyaml.serializer.Serializer;
+
+/**
+ * YAML-JSON translator. The methods may throw either of the runtime exceptions,
+ * YAMLException or JsonSyntaxException.
+ */
+public class YamlJsonTranslator {
+
+    /**
+     * Object to be used to translate between YAML and JsonElement.
+     */
+    private final Gson gson;
+
+    /**
+     * Constructs the object.
+     */
+    public YamlJsonTranslator() {
+        this(new Gson());
+    }
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson the Gson object to be used to serialize and de-serialize
+     */
+    public YamlJsonTranslator(Gson gson) {
+        this.gson = gson;
+    }
+
+    /**
+     * Translates a POJO into a YAML String.
+     *
+     * @param object POJO to be translated
+     * @return YAML representing the original object
+     */
+    public String toYaml(Object object) {
+        StringWriter output = new StringWriter();
+        toYaml(output, object);
+        return output.toString();
+    }
+
+    /**
+     * Serializes a POJO to a writer, as YAML.
+     *
+     * @param target target writer
+     * @param object POJO to be translated
+     */
+    public void toYaml(Writer target, Object object) {
+        DumperOptions dumper = new DumperOptions();
+        Serializer serializer = new Serializer(new Emitter(target, dumper), new Resolver(), dumper, null);
+
+        try {
+            serializer.open();
+            serializer.serialize(makeYaml(toJsonTree(object)));
+            serializer.close();
+
+        } catch (IOException e) {
+            throw new YAMLException(e);
+        }
+    }
+
+    /**
+     * Translates a POJO into a JsonElement.
+     *
+     * @param object POJO to be translated
+     * @return a JsonElement representing the original object
+     */
+    protected JsonElement toJsonTree(Object object) {
+        return gson.toJsonTree(object);
+    }
+
+    /**
+     * Translates a YAML string to a POJO.
+     *
+     * @param yaml YAML string to be translated
+     * @param clazz class of POJO to be created
+     * @return a POJO representing the original YAML
+     */
+    public <T> T fromYaml(String yaml, Class<T> clazz) {
+        return fromYaml(new StringReader(yaml), clazz);
+    }
+
+    /**
+     * Translates a YAML string, read from a reader, into a POJO.
+     *
+     * @param source source of the YAML string to be translated
+     * @param clazz class of POJO to be created
+     * @return a POJO representing the YAML read from the reader
+     */
+    public <T> T fromYaml(Reader source, Class<T> clazz) {
+        Node node = new Yaml().compose(source);
+        return fromJson(makeJson(node), clazz);
+    }
+
+    /**
+     * Translates a JsonElement to a POJO of the given class.
+     *
+     * @param jel element to be translated
+     * @param clazz class of POJO to be created
+     * @return a POJO representing the original element
+     */
+    protected <T> T fromJson(JsonElement jel, Class<T> clazz) {
+        return gson.fromJson(jel, clazz);
+    }
+
+    /**
+     * Converts an arbitrary gson element into a corresponding Yaml node.
+     *
+     * @param jel gson element to be converted
+     * @return a yaml node corresponding to the element
+     */
+    protected Node makeYaml(JsonElement jel) {
+        if (jel.isJsonArray()) {
+            return makeYamlSequence((JsonArray) jel);
+
+        } else if (jel.isJsonObject()) {
+            return makeYamlMap((JsonObject) jel);
+
+        } else if (jel.isJsonPrimitive()) {
+            return makeYamlPrim((JsonPrimitive) jel);
+
+        } else {
+            return new ScalarNode(Tag.NULL, "", null, null, DumperOptions.ScalarStyle.PLAIN);
+        }
+    }
+
+    /**
+     * Converts an arbitrary gson array into a corresponding Yaml sequence.
+     *
+     * @param jel gson element to be converted
+     * @return a yaml node corresponding to the element
+     */
+    protected SequenceNode makeYamlSequence(JsonArray jel) {
+        List<Node> nodes = new ArrayList<>(jel.size());
+        jel.forEach(item -> nodes.add(makeYaml(item)));
+
+        return new SequenceNode(Tag.SEQ, true, nodes, null, null, DumperOptions.FlowStyle.AUTO);
+    }
+
+    /**
+     * Converts an arbitrary gson object into a corresponding Yaml map.
+     *
+     * @param jel gson element to be converted
+     * @return a yaml node corresponding to the element
+     */
+    protected MappingNode makeYamlMap(JsonObject jel) {
+        List<NodeTuple> nodes = new ArrayList<>(jel.size());
+
+        for (Entry<String, JsonElement> entry : jel.entrySet()) {
+            Node key = new ScalarNode(Tag.STR, entry.getKey(), null, null, DumperOptions.ScalarStyle.PLAIN);
+            Node value = makeYaml(entry.getValue());
+
+            nodes.add(new NodeTuple(key, value));
+        }
+
+        return new MappingNode(Tag.MAP, true, nodes, null, null, DumperOptions.FlowStyle.AUTO);
+    }
+
+    /**
+     * Converts an arbitrary gson primitive into a corresponding Yaml scalar.
+     *
+     * @param jel gson element to be converted
+     * @return a yaml node corresponding to the element
+     */
+    protected ScalarNode makeYamlPrim(JsonPrimitive jel) {
+        Tag tag;
+        if (jel.isNumber()) {
+            Class<? extends Number> clazz = jel.getAsNumber().getClass();
+
+            if (clazz == Double.class || clazz == Float.class) {
+                tag = Tag.FLOAT;
+
+            } else {
+                tag = Tag.INT;
+            }
+
+        } else if (jel.isBoolean()) {
+            tag = Tag.BOOL;
+
+        } else {
+            // treat anything else as a string
+            tag = Tag.STR;
+        }
+
+        return new ScalarNode(tag, jel.getAsString(), null, null, DumperOptions.ScalarStyle.PLAIN);
+    }
+
+    /**
+     * Converts an arbitrary Yaml node into a corresponding gson element.
+     *
+     * @param node node to be converted
+     * @return a gson element corresponding to the node
+     */
+    protected JsonElement makeJson(Node node) {
+        if (node instanceof MappingNode) {
+            return makeJsonObject((MappingNode) node);
+
+        } else if (node instanceof SequenceNode) {
+            return makeJsonArray((SequenceNode) node);
+
+        } else {
+            return makeJsonPrim((ScalarNode) node);
+        }
+
+        // yaml doesn't appear to use anchor nodes when decoding so ignore them for now
+    }
+
+    /**
+     * Converts a Yaml sequence into a corresponding gson array.
+     *
+     * @param node node to be converted
+     * @return a gson element corresponding to the node
+     */
+    protected JsonArray makeJsonArray(SequenceNode node) {
+        List<Node> nodes = node.getValue();
+
+        JsonArray array = new JsonArray(nodes.size());
+        nodes.forEach(subnode -> array.add(makeJson(subnode)));
+
+        return array;
+    }
+
+    /**
+     * Converts a Yaml map into a corresponding gson object.
+     *
+     * @param node node to be converted
+     * @return a gson element corresponding to the node
+     */
+    protected JsonObject makeJsonObject(MappingNode node) {
+        JsonObject obj = new JsonObject();
+
+        for (NodeTuple tuple : node.getValue()) {
+            Node key = tuple.getKeyNode();
+            String skey = ((ScalarNode) key).getValue();
+
+            obj.add(skey, makeJson(tuple.getValueNode()));
+        }
+
+        return obj;
+    }
+
+    /**
+     * Converts a Yaml scalar into a corresponding gson primitive.
+     *
+     * @param node node to be converted
+     * @return a gson element corresponding to the node
+     */
+    protected JsonElement makeJsonPrim(ScalarNode node) {
+        try {
+            Tag tag = node.getTag();
+
+            if (tag == Tag.INT) {
+                return new JsonPrimitive(Long.valueOf(node.getValue()));
+
+            } else if (tag == Tag.FLOAT) {
+                return new JsonPrimitive(Double.valueOf(node.getValue()));
+
+            } else if (tag == Tag.BOOL) {
+                return new JsonPrimitive(Boolean.valueOf(node.getValue()));
+
+            } else if (tag == Tag.NULL) {
+                return JsonNull.INSTANCE;
+
+            } else {
+                // treat anything else as a string
+                return new JsonPrimitive(node.getValue());
+            }
+
+        } catch (NumberFormatException ex) {
+            // just treat it as a string
+            return new JsonPrimitive(node.getValue());
+        }
+    }
+}
index 2992933..2a70f85 100644 (file)
@@ -44,6 +44,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import org.junit.Before;
@@ -200,6 +201,22 @@ public class StandardCoderTest {
                         .hasCause(ioe);
     }
 
+    @Test
+    public void testToJsonTree_testFromJsonJsonElementClassT() throws Exception {
+        MyMap map = new MyMap();
+        map.props = new LinkedHashMap<>();
+        map.props.put("jel keyA", "jel valueA");
+        map.props.put("jel keyB", "jel valueB");
+
+        JsonElement json = coder.toJsonTree(map);
+        assertEquals("{'props':{'jel keyA':'jel valueA','jel keyB':'jel valueB'}}".replace('\'', '"'), json.toString());
+
+        Object result = coder.fromJson(json, MyMap.class);
+
+        assertNotNull(result);
+        assertEquals("{jel keyA=jel valueA, jel keyB=jel valueB}", result.toString());
+    }
+
     @Test
     public void testConvertFromDouble() throws Exception {
         String text = "[listA, {keyA=100}, 200]";
@@ -276,7 +293,12 @@ public class StandardCoderTest {
         }
     }
 
-    private static class MyMap {
+    public static class MyMap {
         private Map<String, Object> props;
+
+        @Override
+        public String toString() {
+            return props.toString();
+        }
     }
 }
index 6ec2066..e38c5c9 100644 (file)
 
 package org.onap.policy.common.utils.coder;
 
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 import java.io.File;
-import java.io.IOException;
-import java.io.Writer;
+import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
-import java.util.List;
-import java.util.Map;
-import lombok.EqualsAndHashCode;
 import org.junit.Before;
 import org.junit.Test;
+import org.onap.policy.common.utils.coder.YamlJsonTranslatorTest.Container;
 
 public class StandardYamlCoderTest {
     private static final File YAML_FILE =
-                    new File("src/test/resources/org/onap/policy/common/utils/coder/StandardYamlCoder.yaml");
+                    new File("src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml");
 
     private StandardYamlCoder coder;
     private Container cont;
@@ -59,30 +53,13 @@ public class StandardYamlCoderTest {
     }
 
     @Test
-    public void testToJsonWriterObject() throws Exception {
-        IOException ex = new IOException("expected exception");
+    public void testToJsonWriterObject() throws CoderException {
+        StringWriter wtr = new StringWriter();
+        coder.encode(wtr, cont);
+        String yaml = wtr.toString();
 
-        // writer that throws an exception when the write() method is invoked
-        Writer wtr = new Writer() {
-            @Override
-            public void write(char[] cbuf, int off, int len) throws IOException {
-                throw ex;
-            }
-
-            @Override
-            public void flush() throws IOException {
-                // do nothing
-            }
-
-            @Override
-            public void close() throws IOException {
-                // do nothing
-            }
-        };
-
-        assertThatThrownBy(() -> coder.encode(wtr, cont)).isInstanceOf(CoderException.class);
-
-        wtr.close();
+        Container cont2 = coder.decode(yaml, Container.class);
+        assertEquals(cont, cont2);
     }
 
     @Test
@@ -94,58 +71,15 @@ public class StandardYamlCoderTest {
 
     @Test
     public void testFromJsonReaderClassOfT() {
-        assertNotNull(cont.item);
-        assertTrue(cont.item.boolVal);
-        assertEquals(1000L, cont.item.longVal);
-        assertEquals(1010.1f, cont.item.floatVal, 0.00001);
-
-        assertEquals(4, cont.list.size());
-        assertNull(cont.list.get(1));
-
-        assertEquals(20, cont.list.get(0).intVal);
-        assertEquals("string 30", cont.list.get(0).stringVal);
-        assertNull(cont.list.get(0).nullVal);
-
-        assertEquals(40.0, cont.list.get(2).doubleVal, 0.000001);
-        assertNull(cont.list.get(2).nullVal);
-        assertNotNull(cont.list.get(2).another);
-        assertEquals(50, cont.list.get(2).another.intVal);
-
-        assertTrue(cont.list.get(3).boolVal);
-
-        assertNotNull(cont.map);
-        assertEquals(3, cont.map.size());
-
-        assertNotNull(cont.map.get("itemA"));
-        assertEquals("stringA", cont.map.get("itemA").stringVal);
-
-        assertNotNull(cont.map.get("itemB"));
-        assertEquals("stringB", cont.map.get("itemB").stringVal);
-
-        double dbl = 123456789012345678901234567890.0;
-        assertEquals(dbl, cont.map.get("itemB").doubleVal, 1000.0);
-
-        assertNotNull(cont.map.get("itemC"));
-        assertTrue(cont.map.get("itemC").boolVal);
+        YamlJsonTranslatorTest.verify(cont);
     }
 
-
-    @EqualsAndHashCode
-    public static class Container {
-        private Item item;
-        private List<Item> list;
-        private Map<String, Item> map;
-    }
-
-    @EqualsAndHashCode
-    public static class Item {
-        private boolean boolVal;
-        private int intVal;
-        private long longVal;
-        private double doubleVal;
-        private float floatVal;
-        private String stringVal;
-        private Object nullVal;
-        private Item another;
+    @Test
+    public void testStandardTypeAdapter() throws Exception {
+        String yaml = "abc: def\n";
+        StandardCoderObject sco = coder.fromJson(yaml, StandardCoderObject.class);
+        assertNotNull(sco.getData());
+        assertEquals("{'abc':'def'}".replace('\'', '"'), sco.getData().toString());
+        assertEquals(yaml, coder.toJson(sco));
     }
 }
diff --git a/utils/src/test/java/org/onap/policy/common/utils/coder/YamlJsonTranslatorTest.java b/utils/src/test/java/org/onap/policy/common/utils/coder/YamlJsonTranslatorTest.java
new file mode 100644 (file)
index 0000000..c76b775
--- /dev/null
@@ -0,0 +1,170 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.coder;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Map;
+import lombok.EqualsAndHashCode;
+import org.junit.Before;
+import org.junit.Test;
+import org.yaml.snakeyaml.error.YAMLException;
+
+public class YamlJsonTranslatorTest {
+    private static final File YAML_FILE =
+                    new File("src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml");
+
+    private Container cont;
+    private YamlJsonTranslator translator;
+
+    /**
+     * Creates {@link #translator} and uses it to load {@link #cont}.
+     *
+     * @throws IOException if an error occurs
+     */
+    @Before
+    public void setUp() throws IOException {
+        translator = new YamlJsonTranslator();
+
+        try (FileReader rdr = new FileReader(YAML_FILE)) {
+            cont = translator.fromYaml(rdr, Container.class);
+        }
+    }
+
+    @Test
+    public void testToYamlObject() {
+        String yaml = translator.toYaml(cont);
+
+        Container cont2 = translator.fromYaml(yaml, Container.class);
+        assertEquals(cont, cont2);
+    }
+
+    @Test
+    public void testToYamlWriterObject() throws IOException {
+        IOException ex = new IOException("expected exception");
+
+        // writer that throws an exception when the write() method is invoked
+        Writer wtr = new Writer() {
+            @Override
+            public void write(char[] cbuf, int off, int len) throws IOException {
+                throw ex;
+            }
+
+            @Override
+            public void flush() throws IOException {
+                // do nothing
+            }
+
+            @Override
+            public void close() throws IOException {
+                // do nothing
+            }
+        };
+
+        assertThatThrownBy(() -> translator.toYaml(wtr, cont)).isInstanceOf(YAMLException.class);
+
+        wtr.close();
+    }
+
+    @Test
+    public void testFromYamlStringClassOfT() throws IOException {
+        String yaml = new String(Files.readAllBytes(YAML_FILE.toPath()), StandardCharsets.UTF_8);
+        Container cont2 = translator.fromYaml(yaml, Container.class);
+        assertEquals(cont, cont2);
+    }
+
+    @Test
+    public void testFromYamlReaderClassOfT() {
+        verify(cont);
+    }
+
+    /**
+     * Verifies that the container has contents matching the yaml file.
+     *
+     * @param container container whose contents are to be verified
+     */
+    public static void verify(Container container) {
+        assertNotNull(container.item);
+        assertTrue(container.item.boolVal);
+        assertEquals(1000L, container.item.longVal);
+        assertEquals(1010.1f, container.item.floatVal, 0.00001);
+
+        assertEquals(4, container.list.size());
+        assertNull(container.list.get(1));
+
+        assertEquals(20, container.list.get(0).intVal);
+        assertEquals("string 30", container.list.get(0).stringVal);
+        assertNull(container.list.get(0).nullVal);
+
+        assertEquals(40.0, container.list.get(2).doubleVal, 0.000001);
+        assertNull(container.list.get(2).nullVal);
+        assertNotNull(container.list.get(2).another);
+        assertEquals(50, container.list.get(2).another.intVal);
+
+        assertTrue(container.list.get(3).boolVal);
+
+        assertNotNull(container.map);
+        assertEquals(3, container.map.size());
+
+        assertNotNull(container.map.get("itemA"));
+        assertEquals("stringA", container.map.get("itemA").stringVal);
+
+        assertNotNull(container.map.get("itemB"));
+        assertEquals("stringB", container.map.get("itemB").stringVal);
+
+        double dbl = 123456789012345678901234567890.0;
+        assertEquals(dbl, container.map.get("itemB").doubleVal, 1000.0);
+
+        assertNotNull(container.map.get("itemC"));
+        assertTrue(container.map.get("itemC").boolVal);
+    }
+
+
+    @EqualsAndHashCode
+    public static class Container {
+        protected Item item;
+        protected List<Item> list;
+        protected Map<String, Item> map;
+    }
+
+    @EqualsAndHashCode
+    public static class Item {
+        protected boolean boolVal;
+        protected int intVal;
+        protected long longVal;
+        protected double doubleVal;
+        protected float floatVal;
+        protected String stringVal;
+        protected Object nullVal;
+        protected Item another;
+    }
+}