You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2021/10/25 12:27:58 UTC

[brooklyn-server] 07/15: better support for dates in deserialization and serialization

This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit 0c09579577da1cec99a787cab4d4144e693f7826
Author: Alex Heneveld <al...@cloudsoftcorp.com>
AuthorDate: Wed Oct 20 16:52:58 2021 +0100

    better support for dates in deserialization and serialization
    
    outputs in ISO 8601 syntax, but can read in using many more
---
 .../core/resolve/jackson/BeanWithTypeUtils.java    | 14 ++--
 .../resolve/jackson/CommonTypesSerialization.java  | 80 ++++++++++++++++++
 .../jackson/JsonSymbolDependentDeserializer.java   | 34 +++++++-
 .../org/apache/brooklyn/util/core/units/Range.java |  1 -
 .../BrooklynMiscJacksonSerializationTest.java      | 94 ++++++++++++++++++++++
 .../core/resolve/jackson/MapperTestFixture.java    |  7 +-
 6 files changed, 216 insertions(+), 14 deletions(-)

diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BeanWithTypeUtils.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BeanWithTypeUtils.java
index 5cc11ef..e36eedd 100644
--- a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BeanWithTypeUtils.java
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/BeanWithTypeUtils.java
@@ -18,19 +18,17 @@
  */
 package org.apache.brooklyn.core.resolve.jackson;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
 import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.databind.json.JsonMapper;
 import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
 import com.google.common.annotations.Beta;
 import com.google.common.reflect.TypeToken;
-import java.util.*;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
 import java.util.Map.Entry;
+import java.util.function.Predicate;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.mgmt.ManagementContext;
 import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext;
@@ -44,8 +42,6 @@ import org.apache.brooklyn.util.core.task.Tasks;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.guava.TypeTokens;
 import org.apache.brooklyn.util.javalang.Boxing;
-
-import java.util.function.Predicate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -72,7 +68,7 @@ public class BeanWithTypeUtils {
                         d -> new JsonDeserializerForCommonBrooklynThings(mgmt, d)
                         // see note below, on convert()
                 ).apply(mapper);
-
+        CommonTypesSerialization.apply(mapper);
         return mapper;
     }
 
diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/CommonTypesSerialization.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/CommonTypesSerialization.java
new file mode 100644
index 0000000..78ae968
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/CommonTypesSerialization.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.brooklyn.core.resolve.jackson;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.NonTypedScalarSerializerBase;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Date;
+import org.apache.brooklyn.util.core.json.DurationSerializer;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.brooklyn.util.time.Time;
+
+public class CommonTypesSerialization {
+
+    public static void apply(ObjectMapper mapper) {
+        mapper.registerModule(new SimpleModule()
+                .addSerializer(Duration.class, new DurationSerializer())
+
+                .addSerializer(Date.class, new DateSerializer())
+                .addDeserializer(Date.class, (JsonDeserializer) new DateDeserializer())
+                .addSerializer(Instant.class, new InstantSerializer())
+                .addDeserializer(Instant.class, (JsonDeserializer) new InstantDeserializer())
+        );
+    }
+
+    public static class DateSerializer extends NonTypedScalarSerializerBase<Date> {
+        protected DateSerializer() { super(Date.class); }
+        @Override
+        public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+            gen.writeString(Time.makeIso8601DateString(value));
+        }
+    }
+    public static class DateDeserializer extends JsonSymbolDependentDeserializer {
+        @Override
+        protected Object deserializeToken(JsonParser p) throws IOException {
+            Object v = p.readValueAs(Object.class);
+            if (v instanceof String) return Time.parseDate((String)v);
+            throw new IllegalArgumentException("Cannot deserialize '"+v+"' as Date");
+        }
+    }
+
+    public static class InstantSerializer extends NonTypedScalarSerializerBase<Instant> {
+        protected InstantSerializer() { super(Instant.class); }
+        @Override
+        public void serialize(Instant value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+            gen.writeString(Time.makeIso8601DateStringZ(value));
+        }
+    }
+    public static class InstantDeserializer extends JsonSymbolDependentDeserializer {
+        @Override
+        protected Object deserializeToken(JsonParser p) throws IOException {
+            Object v = p.readValueAs(Object.class);
+            if (v instanceof String) return Time.parseInstant((String)v);
+            throw new IllegalArgumentException("Cannot deserialize '"+v+"' as Instant");
+        }
+    }
+
+}
diff --git a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/JsonSymbolDependentDeserializer.java b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/JsonSymbolDependentDeserializer.java
index 1fa30a6..9f7281e 100644
--- a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/JsonSymbolDependentDeserializer.java
+++ b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/JsonSymbolDependentDeserializer.java
@@ -22,12 +22,25 @@ import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.JsonToken;
 import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.deser.BeanDeserializerFactory;
 import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
+import com.fasterxml.jackson.databind.deser.DeserializerFactory;
+import com.google.common.collect.ImmutableSet;
 import java.io.IOException;
+import java.util.Set;
 import java.util.function.Function;
+import org.apache.brooklyn.util.core.xstream.ImmutableSetConverter;
 
 public class JsonSymbolDependentDeserializer extends JsonDeserializer<Object> implements ContextualDeserializer {
 
+    public static final Set<JsonToken> SIMPLE_TOKENS = ImmutableSet.of(
+            JsonToken.VALUE_STRING,
+            JsonToken.VALUE_NUMBER_FLOAT,
+            JsonToken.VALUE_NUMBER_INT,
+            JsonToken.VALUE_TRUE,
+            JsonToken.VALUE_FALSE,
+            JsonToken.VALUE_NULL
+    );
     protected DeserializationContext ctxt;
     protected BeanProperty beanProp;
     private BeanDescription beanDesc;
@@ -60,9 +73,11 @@ public class JsonSymbolDependentDeserializer extends JsonDeserializer<Object> im
 
         if (p.getCurrentToken() == JsonToken.START_ARRAY) {
             return deserializeArray(p);
+        } else if (SIMPLE_TOKENS.contains(p.getCurrentToken())) {
+            // string
+            return deserializeToken(p);
         } else {
-            // (primitives, string, etc not yet supported)
-
+            // other primitives not yet supported
             // assume object
             return deserializeObject(p);
         }
@@ -83,11 +98,24 @@ public class JsonSymbolDependentDeserializer extends JsonDeserializer<Object> im
         throw new IllegalStateException("List input not supported for "+type);
     }
 
+    protected Object deserializeToken(JsonParser p) throws IOException, JsonProcessingException {
+        return contextualize(getTokenDeserializer()).deserialize(p, ctxt);
+    }
+    protected JsonDeserializer<?> getTokenDeserializer() throws IOException, JsonProcessingException {
+        return getObjectDeserializer();
+    }
+
     protected Object deserializeObject(JsonParser p) throws IOException, JsonProcessingException {
         return contextualize(getObjectDeserializer()).deserialize(p, ctxt);
     }
     protected JsonDeserializer<?> getObjectDeserializer() throws IOException, JsonProcessingException {
-        return ctxt.getFactory().createBeanDeserializer(ctxt, type, getBeanDescription());
+        DeserializerFactory f = ctxt.getFactory();
+        if (f instanceof BeanDeserializerFactory) {
+            // don't recurse, we're likely to just return ourselves
+            return ((BeanDeserializerFactory)f).buildBeanDeserializer(ctxt, type, getBeanDescription());
+        }
+        // will probably cause endless loop; we don't know how to deserialize
+        return f.createBeanDeserializer(ctxt, type, getBeanDescription());
     }
 
 }
diff --git a/core/src/main/java/org/apache/brooklyn/util/core/units/Range.java b/core/src/main/java/org/apache/brooklyn/util/core/units/Range.java
index cb691f7..9c78920 100644
--- a/core/src/main/java/org/apache/brooklyn/util/core/units/Range.java
+++ b/core/src/main/java/org/apache/brooklyn/util/core/units/Range.java
@@ -52,7 +52,6 @@ public class Range extends MutableList<Object> {
         l.forEach(this::add);
     }
 
-    // TODO this could be replaced by a ConstructorMatchingSymbolDependentDeserializer
     public static class RangeDeserializer extends JsonSymbolDependentDeserializer {
         @Override
         protected Object deserializeArray(JsonParser p) throws IOException {
diff --git a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java
index 83b7d29..4a015ef 100644
--- a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynMiscJacksonSerializationTest.java
@@ -19,16 +19,31 @@
 package org.apache.brooklyn.core.resolve.jackson;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
 import com.google.common.reflect.TypeToken;
 import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
 import java.util.Map;
+import java.util.TimeZone;
+import org.apache.brooklyn.api.typereg.RegisteredType;
+import org.apache.brooklyn.core.typereg.BasicBrooklynTypeRegistry;
+import org.apache.brooklyn.core.typereg.BasicTypeImplementationPlan;
+import org.apache.brooklyn.core.typereg.RegisteredTypes;
 import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.javalang.JavaClassNames;
+import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
+import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.time.Duration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 public class BrooklynMiscJacksonSerializationTest implements MapperTestFixture {
@@ -42,6 +57,11 @@ public class BrooklynMiscJacksonSerializationTest implements MapperTestFixture {
         return mapper;
     }
 
+    @BeforeMethod(alwaysRun = true)
+    public void setUp() throws Exception {
+        mapper = null;
+    }
+
     // baseline
 
     static class EmptyObject {}
@@ -100,4 +120,78 @@ public class BrooklynMiscJacksonSerializationTest implements MapperTestFixture {
         Asserts.assertTrue(f1==f2, "different instances for "+f1+" and "+f2);
     }
 
+
+    @Test
+    public void testDurationCustomSerialization() throws Exception {
+        mapper = BeanWithTypeUtils.newSimpleYamlMapper();
+
+        // need these two to get the constructor stuff we want (but _not_ the default duration support)
+        BrooklynRegisteredTypeJacksonSerialization.apply(mapper, null, false, null, true);
+        WrappedValuesSerialization.apply(mapper, null);
+
+        Assert.assertEquals(ser(Duration.FIVE_SECONDS, Duration.class), "nanos: 5000000000");
+        Assert.assertEquals(deser("nanos: 5000000000", Duration.class), Duration.FIVE_SECONDS);
+
+        Asserts.assertFailsWith(() -> deser("5s", Duration.class),
+                e -> e.toString().contains("Duration"));
+
+
+        // custom serializer added as part of standard mapper construction
+
+        mapper = BeanWithTypeUtils.newYamlMapper(null, false, null, true);
+
+        Assert.assertEquals(deser("5s", Duration.class), Duration.FIVE_SECONDS);
+        Assert.assertEquals(deser("nanos: 5000000000", Duration.class), Duration.FIVE_SECONDS);
+
+        Assert.assertEquals(ser(Duration.FIVE_SECONDS, Duration.class), JavaStringEscapes.wrapJavaString("5s"));
+    }
+
+
+    public static class DateTimeBean {
+        String x;
+        Date juDate;
+//        LocalDateTime localDateTime;
+        GregorianCalendar calendar;
+        Instant instant;
+    }
+
+    @Test
+    public void testDateTimeInRegisteredTypes() throws Exception {
+        mapper = BeanWithTypeUtils.newYamlMapper(null, false, null, true);
+//        customMapper.findAndRegisterModules();
+        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+
+        DateTimeBean impl = new DateTimeBean();
+        Asserts.assertEquals(ser(impl, DateTimeBean.class), "{}" );
+
+        impl.x = "foo";
+
+        impl.juDate = new Date(60*1000);
+//        impl.localDateTime = LocalDateTime.of(2020, 1, 1, 12, 0, 0, 0);
+        impl.calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"), Locale.ROOT);
+        impl.calendar.set(2020, 0, 1, 12, 0, 0);
+        impl.calendar.set(GregorianCalendar.MILLISECOND, 0);
+        impl.instant = impl.calendar.toInstant();
+        Asserts.assertEquals(ser(impl, DateTimeBean.class), Strings.lines(
+                "x: \"foo\"",
+                "juDate: \"1970-01-01T00:01:00.000Z\"",
+//                "localDateTime: \"2020-01-01T12:00:00\"",
+                "calendar: \"2020-01-01T12:00:00.000+00:00\"",
+                "instant: \"2020-01-01T12:00:00.000Z\""));
+
+        // ones commented out cannot be parsed
+        DateTimeBean impl2 = deser(Strings.lines(
+                "x: foo",
+                "juDate: 1970-01-01T00:01:00.000+00:00",
+//                "localDateTime: \"2020-01-01T12:00:00\"",
+//                "calendar: \"2020-01-01T12:00:00.000+00:00\"",
+                "instant: 2020-01-01T12:00:00Z",
+                ""
+        ), DateTimeBean.class);
+        Assert.assertEquals( impl2.x, impl.x );
+        Assert.assertEquals( impl2.juDate, impl.juDate );
+//        Assert.assertEquals( impl2.localDateTime, impl.localDateTime );
+//        Assert.assertEquals( impl2.calendar, impl.calendar );
+        Assert.assertEquals( impl2.instant, impl.instant );
+    }
 }
diff --git a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/MapperTestFixture.java b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/MapperTestFixture.java
index 387ee77..497f722 100644
--- a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/MapperTestFixture.java
+++ b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/MapperTestFixture.java
@@ -29,6 +29,7 @@ import org.apache.brooklyn.util.core.task.Tasks;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.javalang.JavaClassNames;
+import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.yaml.Yamls;
 
 public interface MapperTestFixture {
@@ -41,7 +42,11 @@ public interface MapperTestFixture {
 
     default <T> String ser(T v, Class<T> type) {
         try {
-            return mapper().writerFor(type).writeValueAsString(v);
+            String result = mapper().writerFor(type).writeValueAsString(v);
+            // don't care about document separator
+            result = Strings.removeFromStart(result, "---");
+            // or whitespace
+            return result.trim();
         } catch (JsonProcessingException e) {
             throw Exceptions.propagate(e);
         }