Add gson adapters for special field types 90/102890/4
authorJim Hahn <jrh3@att.com>
Tue, 3 Mar 2020 16:55:02 +0000 (11:55 -0500)
committerJim Hahn <jrh3@att.com>
Tue, 3 Mar 2020 22:46:47 +0000 (17:46 -0500)
Added type adapters for Instant, LocalDateTime, and ZonedDateTime.  UUID
seems to work already.
Added new Coder that offers an alternative encoding for Instant.

Issue-ID: POLICY-1625
Signed-off-by: Jim Hahn <jrh3@att.com>
Change-Id: I5230fa7fe955d78c5f2da1316cb1504b5875ea84

13 files changed:
gson/src/main/java/org/onap/policy/common/gson/GsonMessageBodyHandler.java
gson/src/main/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapter.java [new file with mode: 0644]
gson/src/main/java/org/onap/policy/common/gson/InstantTypeAdapter.java [new file with mode: 0644]
gson/src/main/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapter.java [new file with mode: 0644]
gson/src/main/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapter.java [new file with mode: 0644]
gson/src/test/java/org/onap/policy/common/gson/GsonMessageBodyHandlerTest.java
gson/src/test/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapterTest.java [new file with mode: 0644]
gson/src/test/java/org/onap/policy/common/gson/InstantTypeAdapterTest.java [new file with mode: 0644]
gson/src/test/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapterTest.java [new file with mode: 0644]
gson/src/test/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapterTest.java [new file with mode: 0644]
utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java
utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoderInstantAsMillis.java [new file with mode: 0644]
utils/src/test/java/org/onap/policy/common/utils/coder/StandardCoderInstantAsMillisTest.java [new file with mode: 0644]

index 75d58f2..d6e36b3 100644 (file)
@@ -2,7 +2,7 @@
  * ============LICENSE_START=======================================================
  * 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.
@@ -30,6 +30,9 @@ import java.io.OutputStreamWriter;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
@@ -85,7 +88,10 @@ public class GsonMessageBodyHandler implements MessageBodyReader<Object>, Messag
      * @return the configured builder
      */
     public static GsonBuilder configBuilder(GsonBuilder builder) {
-        return builder.disableHtmlEscaping().registerTypeAdapterFactory(new MapDoubleAdapterFactory());
+        return builder.disableHtmlEscaping().registerTypeAdapterFactory(new MapDoubleAdapterFactory())
+                        .registerTypeAdapter(Instant.class, new InstantTypeAdapter())
+                        .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter())
+                        .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter());
     }
 
     @Override
diff --git a/gson/src/main/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapter.java b/gson/src/main/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapter.java
new file mode 100644 (file)
index 0000000..6bcfaac
--- /dev/null
@@ -0,0 +1,58 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * GSON Type Adapter for "Instant" fields, that encodes them as milliseconds.
+ */
+public class InstantAsMillisTypeAdapter extends TypeAdapter<Instant> {
+
+    @Override
+    public void write(JsonWriter out, Instant value) throws IOException {
+        if (value == null) {
+            out.nullValue();
+        } else {
+            long epochMillis = TimeUnit.MILLISECONDS.convert(value.getEpochSecond(), TimeUnit.SECONDS);
+            long nanoMillis = TimeUnit.MILLISECONDS.convert(value.getNano(), TimeUnit.NANOSECONDS);
+            long millis = epochMillis + nanoMillis;
+            out.value(millis);
+        }
+    }
+
+    @Override
+    public Instant read(JsonReader in) throws IOException {
+        if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+        } else {
+            long millis = in.nextLong();
+            return Instant.ofEpochMilli(millis);
+        }
+    }
+}
diff --git a/gson/src/main/java/org/onap/policy/common/gson/InstantTypeAdapter.java b/gson/src/main/java/org/onap/policy/common/gson/InstantTypeAdapter.java
new file mode 100644 (file)
index 0000000..9ebf2ba
--- /dev/null
@@ -0,0 +1,60 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+
+/**
+ * GSON Type Adapter for "Instant" fields, that uses the standard ISO_INSTANT formatter.
+ */
+public class InstantTypeAdapter extends TypeAdapter<Instant> {
+
+    @Override
+    public void write(JsonWriter out, Instant value) throws IOException {
+        if (value == null) {
+            out.nullValue();
+        } else {
+            out.value(value.toString());
+        }
+    }
+
+    @Override
+    public Instant read(JsonReader in) throws IOException {
+        try {
+            if (in.peek() == JsonToken.NULL) {
+                in.nextNull();
+                return null;
+            } else {
+                String text = in.nextString();
+                return Instant.parse(text);
+            }
+
+        } catch (DateTimeParseException e) {
+            throw new IOException("invalid date", e);
+        }
+    }
+}
diff --git a/gson/src/main/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapter.java b/gson/src/main/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapter.java
new file mode 100644 (file)
index 0000000..2b297cb
--- /dev/null
@@ -0,0 +1,64 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+/**
+ * GSON Type Adapter for "LocalDateTime" fields, that uses the standard
+ * ISO_LOCAL_DATE_TIME formatter.
+ */
+public class LocalDateTimeTypeAdapter extends TypeAdapter<LocalDateTime> {
+    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+
+    @Override
+    public LocalDateTime read(JsonReader in) throws IOException {
+        try {
+            if (in.peek() == JsonToken.NULL) {
+                in.nextNull();
+                return null;
+            } else {
+                return LocalDateTime.parse(in.nextString(), FORMATTER);
+            }
+
+        } catch (DateTimeParseException e) {
+            throw new JsonParseException("invalid date", e);
+        }
+    }
+
+    @Override
+    public void write(JsonWriter out, LocalDateTime value) throws IOException {
+        if (value == null) {
+            out.nullValue();
+        } else {
+            String text = value.format(FORMATTER);
+            out.value(text);
+        }
+    }
+}
diff --git a/gson/src/main/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapter.java b/gson/src/main/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapter.java
new file mode 100644 (file)
index 0000000..147fb03
--- /dev/null
@@ -0,0 +1,64 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+/**
+ * GSON Type Adapter for "ZonedDateTime" fields, that uses the standard
+ * ISO_ZONED_DATE_TIME formatter.
+ */
+public class ZonedDateTimeTypeAdapter extends TypeAdapter<ZonedDateTime> {
+    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME;
+
+    @Override
+    public ZonedDateTime read(JsonReader in) throws IOException {
+        try {
+            if (in.peek() == JsonToken.NULL) {
+                in.nextNull();
+                return null;
+            } else {
+                return ZonedDateTime.parse(in.nextString(), FORMATTER);
+            }
+
+        } catch (DateTimeParseException e) {
+            throw new JsonParseException("invalid date", e);
+        }
+    }
+
+    @Override
+    public void write(JsonWriter out, ZonedDateTime value) throws IOException {
+        if (value == null) {
+            out.nullValue();
+        } else {
+            String text = value.format(FORMATTER);
+            out.value(text);
+        }
+    }
+}
index f1740ac..c05a1e5 100644 (file)
@@ -2,7 +2,7 @@
  * ============LICENSE_START=======================================================
  * 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.
 
 package org.onap.policy.common.gson;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.UUID;
 import javax.ws.rs.core.MediaType;
+import lombok.ToString;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -160,6 +169,32 @@ public class GsonMessageBodyHandlerTest {
         assertEquals(12.5, map.props.get("doubleVal"));
     }
 
+    @Test
+    public void testInterestingFields() throws IOException {
+        InterestingFields data = new InterestingFields();
+        data.instant = Instant.ofEpochMilli(1583249713500L);
+        data.uuid = UUID.fromString("a850cb9f-3c5e-417c-abfd-0679cdcd1ab0");
+        data.localDate = LocalDateTime.of(2020, 2, 3, 4, 5, 6, 789000000);
+        data.zonedDate = ZonedDateTime.of(2020, 2, 3, 4, 5, 6, 789000000, ZoneId.of("US/Eastern"));
+
+        ByteArrayOutputStream outstr = new ByteArrayOutputStream();
+        hdlr.writeTo(data, data.getClass(), data.getClass(), null, null, null, outstr);
+
+        // ensure fields are encoded as expected
+
+        // @formatter:off
+        assertThat(outstr.toString(StandardCharsets.UTF_8))
+                            .contains("\"2020-03-03T15:35:13.500Z\"")
+                            .contains("\"2020-02-03T04:05:06.789\"")
+                            .contains("\"2020-02-03T04:05:06.789-05:00[US/Eastern]\"")
+                            .contains("a850cb9f-3c5e-417c-abfd-0679cdcd1ab0");
+        // @formatter:on
+
+        Object obj2 = hdlr.readFrom(Object.class, data.getClass(), null, null, null,
+                        new ByteArrayInputStream(outstr.toByteArray()));
+        assertEquals(data.toString(), obj2.toString());
+    }
+
 
     public static class MyObject {
         private int id;
@@ -186,4 +221,12 @@ public class GsonMessageBodyHandlerTest {
             return props.toString();
         }
     }
+
+    @ToString
+    private static class InterestingFields {
+        private LocalDateTime localDate;
+        private Instant instant;
+        private UUID uuid;
+        private ZonedDateTime zonedDate;
+    }
 }
diff --git a/gson/src/test/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapterTest.java b/gson/src/test/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..c48919a
--- /dev/null
@@ -0,0 +1,66 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.time.Instant;
+import lombok.ToString;
+import org.junit.Test;
+
+public class InstantAsMillisTypeAdapterTest {
+    private static Gson gson =
+                    new GsonBuilder().registerTypeAdapter(Instant.class, new InstantAsMillisTypeAdapter()).create();
+
+    @Test
+    public void test() {
+        InterestingFields data = new InterestingFields();
+        data.instant = Instant.ofEpochMilli(1583249713500L);
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("nanos").contains("1583249713500").doesNotContain("\"1583249713500\"")
+                        .doesNotContain("T");
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null output
+        data.instant = null;
+        json = gson.toJson(data);
+        data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null input
+        data2 = gson.fromJson("{\"instant\":null}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+    }
+
+
+    @ToString
+    private static class InterestingFields {
+        private Instant instant;
+    }
+}
diff --git a/gson/src/test/java/org/onap/policy/common/gson/InstantTypeAdapterTest.java b/gson/src/test/java/org/onap/policy/common/gson/InstantTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..97219d0
--- /dev/null
@@ -0,0 +1,73 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.time.Instant;
+import lombok.ToString;
+import org.junit.Test;
+
+public class InstantTypeAdapterTest {
+    private static Gson gson =
+                    new GsonBuilder().registerTypeAdapter(Instant.class, new InstantTypeAdapter()).create();
+
+    @Test
+    public void test() {
+        InterestingFields data = new InterestingFields();
+        data.instant = Instant.ofEpochMilli(1583249713500L);
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("nanos").contains("\"2020-03-03T15:35:13.500Z\"")
+                        .doesNotContain("1583249713500");
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // try when the date-time string is invalid
+        String json2 = json.replace("2020", "invalid-date");
+        assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class)
+                        .hasMessageContaining("invalid date");
+
+        // null output
+        data.instant = null;
+        json = gson.toJson(data);
+        data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null input
+        data2 = gson.fromJson("{\"instant\":null}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+    }
+
+
+    @ToString
+    private static class InterestingFields {
+        private Instant instant;
+    }
+}
diff --git a/gson/src/test/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapterTest.java b/gson/src/test/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..2778a4b
--- /dev/null
@@ -0,0 +1,72 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.time.LocalDateTime;
+import lombok.ToString;
+import org.junit.Test;
+
+public class LocalDateTimeTypeAdapterTest {
+    private static Gson gson =
+                    new GsonBuilder().registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter()).create();
+
+    @Test
+    public void test() {
+        InterestingFields data = new InterestingFields();
+        data.date = LocalDateTime.of(2020, 2, 3, 4, 5, 6, 789000000);
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("year").contains("\"2020-02-03T04:05:06.789\"");
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // try when the date-time string is invalid
+        String json2 = json.replace("2020", "invalid-date");
+        assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class)
+                        .hasMessageContaining("invalid date");
+
+        // null output
+        data.date = null;
+        json = gson.toJson(data);
+        data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null input
+        data2 = gson.fromJson("{\"date\":null}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+    }
+
+
+    @ToString
+    private static class InterestingFields {
+        private LocalDateTime date;
+    }
+}
diff --git a/gson/src/test/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapterTest.java b/gson/src/test/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..766a979
--- /dev/null
@@ -0,0 +1,73 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import lombok.ToString;
+import org.junit.Test;
+
+public class ZonedDateTimeTypeAdapterTest {
+    private static Gson gson =
+                    new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter()).create();
+
+    @Test
+    public void test() {
+        InterestingFields data = new InterestingFields();
+        data.date = ZonedDateTime.of(2020, 2, 3, 4, 5, 6, 789000000, ZoneId.of("US/Eastern"));
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("year").contains("\"2020-02-03T04:05:06.789-05:00[US/Eastern]\"");
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // try when the date-time string is invalid
+        String json2 = json.replace("2020", "invalid-date");
+        assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class)
+                        .hasMessageContaining("invalid date");
+
+        // null output
+        data.date = null;
+        json = gson.toJson(data);
+        data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null input
+        data2 = gson.fromJson("{\"date\":null}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+    }
+
+
+    @ToString
+    private static class InterestingFields {
+        private ZonedDateTime date;
+    }
+}
index 9d444ca..2548dea 100644 (file)
@@ -363,7 +363,7 @@ public class StandardCoder implements Coder {
     /**
      * Adapter for standard objects.
      */
-    private static class StandardTypeAdapter extends TypeAdapter<StandardCoderObject> {
+    protected static class StandardTypeAdapter extends TypeAdapter<StandardCoderObject> {
 
         /**
          * Used to read/write a JsonElement.
diff --git a/utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoderInstantAsMillis.java b/utils/src/main/java/org/onap/policy/common/utils/coder/StandardCoderInstantAsMillis.java
new file mode 100644 (file)
index 0000000..fbb53b9
--- /dev/null
@@ -0,0 +1,125 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.coder;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import java.io.Reader;
+import java.io.Writer;
+import java.time.Instant;
+import lombok.AccessLevel;
+import lombok.Getter;
+import org.onap.policy.common.gson.GsonMessageBodyHandler;
+import org.onap.policy.common.gson.InstantAsMillisTypeAdapter;
+
+/**
+ * JSON encoder and decoder using the "standard" mechanism, but encodes Instant fields as
+ * Long milliseconds.
+ */
+public class StandardCoderInstantAsMillis extends StandardCoder {
+
+    /**
+     * Gson object used to encode and decode messages.
+     */
+    @Getter(AccessLevel.PROTECTED)
+    private static final Gson GSON;
+
+    /**
+     * Gson object used to encode messages in "pretty" format.
+     */
+    @Getter(AccessLevel.PROTECTED)
+    private static final Gson GSON_PRETTY;
+
+    static {
+        GsonBuilder builder = GsonMessageBodyHandler
+                        .configBuilder(new GsonBuilder().registerTypeAdapter(StandardCoderObject.class,
+                                        new StandardTypeAdapter()))
+                        .registerTypeAdapter(Instant.class, new InstantAsMillisTypeAdapter());
+
+        GSON = builder.create();
+        GSON_PRETTY = builder.setPrettyPrinting().create();
+    }
+
+    /**
+     * Constructs the object.
+     */
+    public StandardCoderInstantAsMillis() {
+        super();
+    }
+
+    @Override
+    protected String toPrettyJson(Object object) {
+        return GSON_PRETTY.toJson(object);
+    }
+
+    @Override
+    public StandardCoderObject toStandard(Object object) throws CoderException {
+        try {
+            return new StandardCoderObject(GSON.toJsonTree(object));
+
+        } catch (RuntimeException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    @Override
+    public <T> T fromStandard(StandardCoderObject sco, Class<T> clazz) throws CoderException {
+        try {
+            return GSON.fromJson(sco.getData(), clazz);
+
+        } catch (RuntimeException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    // the remaining methods are wrappers that can be overridden by junit tests
+
+    @Override
+    protected JsonElement toJsonTree(Object object) {
+        return GSON.toJsonTree(object);
+    }
+
+    @Override
+    protected String toJson(Object object) {
+        return GSON.toJson(object);
+    }
+
+    @Override
+    protected void toJson(Writer target, Object object) {
+        GSON.toJson(object, object.getClass(), target);
+    }
+
+    @Override
+    protected <T> T fromJson(JsonElement json, Class<T> clazz) {
+        return convertFromDouble(clazz, GSON.fromJson(json, clazz));
+    }
+
+    @Override
+    protected <T> T fromJson(String json, Class<T> clazz) {
+        return convertFromDouble(clazz, GSON.fromJson(json, clazz));
+    }
+
+    @Override
+    protected <T> T fromJson(Reader source, Class<T> clazz) {
+        return convertFromDouble(clazz, GSON.fromJson(source, clazz));
+    }
+}
diff --git a/utils/src/test/java/org/onap/policy/common/utils/coder/StandardCoderInstantAsMillisTest.java b/utils/src/test/java/org/onap/policy/common/utils/coder/StandardCoderInstantAsMillisTest.java
new file mode 100644 (file)
index 0000000..ec977da
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP PAP
+ * ================================================================================
+ * 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.
+ * 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.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import com.google.gson.JsonElement;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import lombok.ToString;
+import org.junit.Before;
+import org.junit.Test;
+
+public class StandardCoderInstantAsMillisTest {
+    private static final long INSTANT_MILLIS = 1583249713500L;
+    private static final String INSTANT_TEXT = String.valueOf(INSTANT_MILLIS);
+
+    private StandardCoder coder;
+
+    @Before
+    public void setUp() {
+        coder = new StandardCoderInstantAsMillis();
+    }
+
+    @Test
+    public void testConvert() throws CoderException {
+        MyObject obj = makeObject();
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> map = coder.convert(obj, LinkedHashMap.class);
+
+        assertThat(map.toString()).contains(INSTANT_TEXT);
+
+        MyObject obj2 = coder.convert(map, MyObject.class);
+        assertEquals(obj.toString(), obj2.toString());
+    }
+
+    @Test
+    public void testEncodeDecode() throws CoderException {
+        MyObject obj = makeObject();
+        assertThat(coder.encode(obj, false)).contains(INSTANT_TEXT);
+        assertThat(coder.encode(obj, true)).contains(INSTANT_TEXT);
+
+        String json = coder.encode(obj);
+        MyObject obj2 = coder.decode(json, MyObject.class);
+        assertEquals(obj.toString(), obj2.toString());
+
+        StringWriter wtr = new StringWriter();
+        coder.encode(wtr, obj);
+        json = wtr.toString();
+        assertThat(json).contains(INSTANT_TEXT);
+
+        StringReader rdr = new StringReader(json);
+        obj2 = coder.decode(rdr, MyObject.class);
+        assertEquals(obj.toString(), obj2.toString());
+    }
+
+    @Test
+    public void testJson() {
+        MyObject obj = makeObject();
+        assertThat(coder.toPrettyJson(obj)).contains(INSTANT_TEXT);
+    }
+
+    @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]";
+        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 = makeObject();
+        StandardCoderObject sco = coder.toStandard(obj);
+        assertNotNull(sco.getData());
+        assertEquals("{'abc':'xyz','instant':1583249713500}".replace('\'', '"'), sco.getData().toString());
+
+        // class instead of object -> exception
+        assertThatThrownBy(() -> coder.toStandard(String.class)).isInstanceOf(CoderException.class);
+    }
+
+    @Test
+    public void testFromStandard() throws Exception {
+        MyObject obj = new MyObject();
+        obj.abc = "pdq";
+        StandardCoderObject sco = coder.toStandard(obj);
+
+        MyObject obj2 = coder.fromStandard(sco, MyObject.class);
+        assertEquals(obj.toString(), obj2.toString());
+
+        // null class -> exception
+        assertThatThrownBy(() -> coder.fromStandard(sco, null)).isInstanceOf(CoderException.class);
+    }
+
+
+    private MyObject makeObject() {
+        MyObject obj = new MyObject();
+        obj.abc = "xyz";
+        obj.instant = Instant.ofEpochMilli(INSTANT_MILLIS);
+        return obj;
+    }
+
+
+    @ToString
+    private static class MyObject {
+        private String abc;
+        private Instant instant;
+    }
+
+    public static class MyMap {
+        private Map<String, Object> props;
+
+        @Override
+        public String toString() {
+            return props.toString();
+        }
+    }
+}