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 <[email protected]> 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); }
