Convert double to int when decoding via gson 14/91614/3
authorJim Hahn <jrh3@att.com>
Wed, 17 Jul 2019 16:05:34 +0000 (12:05 -0400)
committerJim Hahn <jrh3@att.com>
Wed, 17 Jul 2019 18:46:31 +0000 (14:46 -0400)
Refactored MapDoubleAdapterFactory, extracting DoubleConverter to be
used by code needing to convert Double to Integer/Long after being
decoded by GSON.
Enhanced StandardCoder to automatically use the above converter if
the desired class is a generic Object.

Change-Id: I1d4e83910de41ceda383f257bfea706db2b8fbbe
Issue-ID: POLICY-1919
Signed-off-by: Jim Hahn <jrh3@att.com>
gson/src/main/java/org/onap/policy/common/gson/DoubleConverter.java [new file with mode: 0644]
gson/src/main/java/org/onap/policy/common/gson/MapDoubleAdapterFactory.java
gson/src/test/java/org/onap/policy/common/gson/DoubleConverterTest.java [new file with mode: 0644]
gson/src/test/java/org/onap/policy/common/gson/MapDoubleAdapterFactoryTest.java
utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java
utils/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java

diff --git a/gson/src/main/java/org/onap/policy/common/gson/DoubleConverter.java b/gson/src/main/java/org/onap/policy/common/gson/DoubleConverter.java
new file mode 100644 (file)
index 0000000..81803ff
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * ============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.gson;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * Converter for Double values. By default, GSON treats all Objects that are numbers, as
+ * Double. This converts Doubles to Integer or Long, if possible. It converts stand-alone
+ * Doubles, as well as those found within Arrays and Maps.
+ */
+public class DoubleConverter {
+
+    private DoubleConverter() {
+        // do nothing
+    }
+
+    /**
+     * Performs in-place conversion of all values in a list.
+     *
+     * @param list the list whose values are to be converted
+     */
+    public static void convertFromDouble(List<Object> list) {
+        if (list == null) {
+            return;
+        }
+
+        List<Object> original = new ArrayList<>(list);
+
+        list.clear();
+        original.forEach(item -> list.add(convertFromDouble(item)));
+    }
+
+    /**
+     * Performs in-place conversion of all values in a map.
+     *
+     * @param map the map whose values are to be converted
+     */
+    public static void convertFromDouble(Map<String, Object> map) {
+        if (map == null) {
+            return;
+        }
+
+        Set<Entry<String, Object>> set = map.entrySet();
+
+        for (Entry<String, Object> entry : set) {
+            entry.setValue(convertFromDouble(entry.getValue()));
+        }
+    }
+
+    /**
+     * Converts a value. If the value is a List, then it recursively converts the
+     * entries of the List. Likewise with a map, however, the map is converted in place.
+     *
+     * @param value value to be converted
+     * @return the converted value
+     */
+    @SuppressWarnings({"unchecked"})
+    public static Object convertFromDouble(Object value) {
+        if (value == null) {
+            return value;
+        }
+
+        if (value instanceof List) {
+            convertFromDouble((List<Object>) value);
+            return value;
+        }
+
+        if (value instanceof Map) {
+            convertFromDouble((Map<String, Object>) value);
+            return value;
+        }
+
+        if (!(value instanceof Double)) {
+            return value;
+        }
+
+        Double num = (Double) value;
+        long longval = num.longValue();
+
+        if (Double.compare(num.doubleValue(), longval) != 0) {
+            // it isn't integral - return unchanged value
+            return value;
+        }
+
+        // it's integral - determine if it's an integer or a long
+        int intval = (int) longval;
+
+        if (intval == longval) {
+            return intval;
+        }
+
+        return longval;
+    }
+}
index e56f9dd..bd03199 100644 (file)
@@ -29,33 +29,47 @@ import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
+import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
 
 /**
- * Adapter factory for Map&lt;String,Object&gt;. By default, GSON treats all Objects, that
- * are numbers, as Double. This walks the map and converts Doubles to Integer or Long, if
+ * Adapter factory for Map&lt;String,Object&gt; and List&lt;String&gt;. By default, GSON treats all Objects, that
+ * are numbers, as Double. This recursively walks a map/list and converts Doubles to Integer or Long, if
  * possible.
  */
 public class MapDoubleAdapterFactory implements TypeAdapterFactory {
 
     @Override
     public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
-        if (type.getRawType() != Map.class) {
+        if (!isMapType(type) && !isListType(type)) {
             return null;
         }
 
+        TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
+
+        return new MapAdapter<>(delegate);
+    }
+
+    private <T> boolean isMapType(TypeToken<T> type) {
+        if (type.getRawType() != Map.class) {
+            return false;
+        }
+
         Type[] actualParams = ((ParameterizedType) type.getType()).getActualTypeArguments();
 
         // only supports Map<String,Object>
-        if (actualParams[0] != String.class || actualParams[1] != Object.class) {
-            return null;
+        return (actualParams[0] == String.class && actualParams[1] == Object.class);
+    }
+
+    private <T> boolean isListType(TypeToken<T> type) {
+        if (type.getRawType() != List.class) {
+            return false;
         }
 
-        TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
+        Type[] actualParams = ((ParameterizedType) type.getType()).getActualTypeArguments();
 
-        return new MapAdapter<>(delegate);
+        // only supports List<Object>
+        return (actualParams[0] == Object.class);
     }
 
     /**
@@ -88,65 +102,9 @@ public class MapDoubleAdapterFactory implements TypeAdapterFactory {
         public T read(JsonReader in) throws IOException {
             T value = delegate.read(in);
 
-            @SuppressWarnings("rawtypes")
-            Map map = (Map) value;
-
-            convertFromDouble(map);
+            DoubleConverter.convertFromDouble(value);
 
             return value;
         }
-
-        /**
-         * Performs conversion of all values in a map.
-         *
-         * @param map the map whose values are to be converted
-         */
-        @SuppressWarnings("rawtypes")
-        private void convertFromDouble(Map map) {
-
-            @SuppressWarnings("unchecked")
-            Set<Entry> set = map.entrySet();
-
-            for (Entry entry : set) {
-                convertFromDouble(entry);
-            }
-        }
-
-        /**
-         * Converts an entry's value. If the value is a Map, then it recursively converts
-         * the entries of the map.
-         *
-         * @param entry entry whose value is to be converted
-         */
-        @SuppressWarnings({"unchecked", "rawtypes"})
-        private void convertFromDouble(Entry entry) {
-            Object obj = entry.getValue();
-
-            if (obj instanceof Map) {
-                convertFromDouble((Map) obj);
-                return;
-            }
-
-            if (!(obj instanceof Double)) {
-                return;
-            }
-
-            Double num = (Double) obj;
-            long longval = num.longValue();
-
-            if (Double.compare(num.doubleValue(), longval) == 0) {
-                // it's integral - determine if it's an integer or a long
-                int intval = (int) longval;
-
-                if (intval == longval) {
-                    // it fits in an integer
-                    entry.setValue(intval);
-
-                } else {
-                    // doesn't fit in an integer - must be a long
-                    entry.setValue(longval);
-                }
-            }
-        }
     }
 }
diff --git a/gson/src/test/java/org/onap/policy/common/gson/DoubleConverterTest.java b/gson/src/test/java/org/onap/policy/common/gson/DoubleConverterTest.java
new file mode 100644 (file)
index 0000000..c81a5cd
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * ============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.gson;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+
+public class DoubleConverterTest {
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testConvertFromDoubleObject() {
+        // these should be unchanged
+        assertNull(DoubleConverter.convertFromDouble((Object) null));
+        assertEquals("hello", DoubleConverter.convertFromDouble("hello"));
+        assertEquals("10.0", DoubleConverter.convertFromDouble("10.0"));
+        assertEquals(12.5, DoubleConverter.convertFromDouble(12.5));
+        assertEquals(12, DoubleConverter.convertFromDouble(12));
+        assertEquals(12L, DoubleConverter.convertFromDouble(12L));
+
+        // positive and negative int
+        assertEquals(10, DoubleConverter.convertFromDouble(10.0));
+        assertEquals(-10, DoubleConverter.convertFromDouble(-10.0));
+
+        // positive and negative long
+        assertEquals(100000000000L, DoubleConverter.convertFromDouble(100000000000.0));
+        assertEquals(-100000000000L, DoubleConverter.convertFromDouble(-100000000000.0));
+
+        // list
+        List<Object> list = new ArrayList<>();
+        list.add("list");
+        list.add(null);
+        list.add(21.0);
+        list = (List<Object>) DoubleConverter.convertFromDouble((Object) list);
+        assertEquals("[list, null, 21]", list.toString());
+
+        // map
+        Map<String,Object> map = new LinkedHashMap<>();
+        map.put("map-A", "map-value");
+        map.put("map-B", null);
+        map.put("map-C", 22.0);
+        map = (Map<String, Object>) DoubleConverter.convertFromDouble((Object) map);
+        assertEquals("{map-A=map-value, map-B=null, map-C=22}", map.toString());
+    }
+
+    @Test
+    public void testConvertFromDoubleList() {
+        // null is ok
+        DoubleConverter.convertFromDouble((List<Object>) null);
+
+        List<Object> list = new ArrayList<>();
+        list.add("world");
+        list.add(20.0);
+
+        List<Object> nested = new ArrayList<>();
+        list.add(nested);
+        nested.add(30.0);
+
+        DoubleConverter.convertFromDouble(list);
+
+        assertEquals("[world, 20, [30]]", list.toString());
+    }
+
+    @Test
+    public void testConvertFromDoubleMap() {
+        // null is ok
+        DoubleConverter.convertFromDouble((Map<String,Object>) null);
+
+        Map<String,Object> map = new LinkedHashMap<>();
+        map.put("keyA", "valueA");
+        map.put("keyB", 200.0);
+
+        Map<String,Object> nested = new LinkedHashMap<>();
+        map.put("keyC", nested);
+        nested.put("nested-key", 201.0);
+
+        DoubleConverter.convertFromDouble(map);
+        assertEquals("{keyA=valueA, keyB=200, keyC={nested-key=201}}", map.toString());
+    }
+}
index 7171d26..79631c5 100644 (file)
@@ -24,8 +24,10 @@ import static org.junit.Assert.assertEquals;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import org.junit.Test;
 
@@ -34,84 +36,130 @@ public class MapDoubleAdapterFactoryTest {
 
     @Test
     @SuppressWarnings("unchecked")
-    public void test() {
+    public void testMap() {
         MyMap map = new MyMap();
-        map.props = new HashMap<>();
-        map.props.put("plainString", "def");
-        map.props.put("posInt", 10);
-        map.props.put("negInt", -10);
-        map.props.put("doubleVal", 12.5);
-        map.props.put("posLong", 100000000000L);
-        map.props.put("negLong", -100000000000L);
+        map.data = new HashMap<>();
+        map.data.put("plainString", "def");
+        map.data.put("posInt", 10);
+        map.data.put("negInt", -10);
+        map.data.put("doubleVal", 12.5);
+        map.data.put("posLong", 100000000000L);
+        map.data.put("negLong", -100000000000L);
 
         Map<String, Object> nested = new LinkedHashMap<>();
-        map.props.put("nestedMap", nested);
+        map.data.put("nestedMap", nested);
         nested.put("nestedString", "world");
         nested.put("nestedInt", 50);
 
         String json = gson.toJson(map);
 
-        map.props.clear();
+        map.data.clear();
         map = gson.fromJson(json, MyMap.class);
 
         assertEquals(json, gson.toJson(map));
 
-        assertEquals(10, map.props.get("posInt"));
-        assertEquals(-10, map.props.get("negInt"));
-        assertEquals(100000000000L, map.props.get("posLong"));
-        assertEquals(-100000000000L, map.props.get("negLong"));
-        assertEquals(12.5, map.props.get("doubleVal"));
-        assertEquals(nested, map.props.get("nestedMap"));
+        assertEquals(10, map.data.get("posInt"));
+        assertEquals(-10, map.data.get("negInt"));
+        assertEquals(100000000000L, map.data.get("posLong"));
+        assertEquals(-100000000000L, map.data.get("negLong"));
+        assertEquals(12.5, map.data.get("doubleVal"));
+        assertEquals(nested, map.data.get("nestedMap"));
 
-        nested = (Map<String, Object>) map.props.get("nestedMap");
+        nested = (Map<String, Object>) map.data.get("nestedMap");
         assertEquals(50, nested.get("nestedInt"));
     }
 
+    @Test
+    public void testList() {
+        MyList list = new MyList();
+        list.data = new ArrayList<>();
+        list.data.add("ghi");
+        list.data.add(100);
+
+        List<Object> nested = new ArrayList<>();
+        list.data.add(nested);
+        nested.add("world2");
+        nested.add(500);
+
+        String json = gson.toJson(list);
+
+        list.data.clear();
+        list = gson.fromJson(json, MyList.class);
+
+        assertEquals(json, gson.toJson(list));
+
+        assertEquals("[ghi, 100, [world2, 500]]", list.data.toString());
+    }
+
     @Test
     public void test_ValueIsNotObject() {
         MyDoubleMap map = new MyDoubleMap();
-        map.props = new LinkedHashMap<>();
-        map.props.put("plainDouble", 13.5);
-        map.props.put("doubleAsInt", 100.0);
+        map.data = new LinkedHashMap<>();
+        map.data.put("plainDouble", 13.5);
+        map.data.put("doubleAsInt", 100.0);
 
         String json = gson.toJson(map);
 
-        map.props.clear();
+        map.data.clear();
         map = gson.fromJson(json, MyDoubleMap.class);
 
         // everything should still be Double - check by simply accessing
-        map.props.get("plainDouble");
-        map.props.get("doubleAsInt");
+        map.data.get("plainDouble");
+        map.data.get("doubleAsInt");
     }
 
     @Test
     public void test_KeyIsNotString() {
         MyObjectMap map = new MyObjectMap();
 
-        map.props = new LinkedHashMap<>();
-        map.props.put("plainDouble2", 14.5);
-        map.props.put("doubleAsInt2", 200.0);
+        map.data = new LinkedHashMap<>();
+        map.data.put("plainDouble2", 14.5);
+        map.data.put("doubleAsInt2", 200.0);
 
         String json = gson.toJson(map);
 
-        map.props.clear();
+        map.data.clear();
         map = gson.fromJson(json, MyObjectMap.class);
 
         // everything should still be Double
-        assertEquals(14.5, map.props.get("plainDouble2"));
-        assertEquals(200.0, map.props.get("doubleAsInt2"));
+        assertEquals(14.5, map.data.get("plainDouble2"));
+        assertEquals(200.0, map.data.get("doubleAsInt2"));
+    }
+
+    @Test
+    public void test_ListValueIsNotObject() {
+        MyDoubleList list = new MyDoubleList();
+        list.data = new ArrayList<>();
+        list.data.add(13.5);
+        list.data.add(100.0);
+
+        String json = gson.toJson(list);
+
+        list.data.clear();
+        list = gson.fromJson(json, MyDoubleList.class);
+
+        // everything should still be Double - check by simply accessing
+        assertEquals("[13.5, 100.0]", list.data.toString());
     }
 
     private static class MyMap {
-        private Map<String, Object> props;
+        private Map<String, Object> data;
     }
 
     private static class MyDoubleMap {
-        private Map<String, Double> props;
+        private Map<String, Double> data;
     }
 
     private static class MyObjectMap {
-        private Map<Object, Object> props;
+        private Map<Object, Object> data;
+    }
+
+    private static class MyList {
+        private List<Object> data;
+    }
+
+    private static class MyDoubleList {
+        private List<Double> data;
     }
 
 }
index 1c65be8..80eddb8 100644 (file)
@@ -38,6 +38,7 @@ import java.io.OutputStreamWriter;
 import java.io.Reader;
 import java.io.Writer;
 import java.nio.charset.StandardCharsets;
+import org.onap.policy.common.gson.DoubleConverter;
 import org.onap.policy.common.gson.MapDoubleAdapterFactory;
 
 /**
@@ -238,7 +239,7 @@ public class StandardCoder implements Coder {
      * @return the object represented by the given json string
      */
     protected <T> T fromJson(String json, Class<T> clazz) {
-        return GSON.fromJson(json, clazz);
+        return convertFromDouble(clazz, GSON.fromJson(json, clazz));
     }
 
     /**
@@ -249,7 +250,24 @@ public class StandardCoder implements Coder {
      * @return the object represented by the given json string
      */
     protected <T> T fromJson(Reader source, Class<T> clazz) {
-        return GSON.fromJson(source, clazz);
+        return convertFromDouble(clazz, GSON.fromJson(source, clazz));
+    }
+
+    /**
+     * Converts a value from Double to Integer/Long, walking the value's contents if it's
+     * a List/Map. Only applies if the specified class refers to the Object class.
+     * Otherwise, it leaves the value unchanged.
+     *
+     * @param clazz class of object to be decoded
+     * @param value value to be converted
+     * @return the converted value
+     */
+    private <T> T convertFromDouble(Class<T> clazz, T value) {
+        if (clazz != Object.class) {
+            return value;
+        }
+
+        return clazz.cast(DoubleConverter.convertFromDouble(value));
     }
 
     /**
index e8e3bb6..2992933 100644 (file)
@@ -200,6 +200,15 @@ public class StandardCoderTest {
                         .hasCause(ioe);
     }
 
+    @Test
+    public void testConvertFromDouble() throws Exception {
+        String text = "[listA, {keyA=100}, 200]";
+        assertEquals(text, coder.decode(text, Object.class).toString());
+
+        text = "{keyB=200}";
+        assertEquals(text, coder.decode(text, Object.class).toString());
+    }
+
     @Test
     public void testToStandard() throws Exception {
         MyObject obj = new MyObject();