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);
}