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 4660a30ce04a3d168098cee68a5ecef286337938 Author: Alex Heneveld <[email protected]> AuthorDate: Fri May 26 10:53:28 2023 +0100 permit jackson to be used to coerce strings and primitives (previously only partially permitted) --- .../core/resolve/jackson/BeanWithTypeUtils.java | 26 ++++-- .../brooklyn/util/core/flags/TypeCoercions.java | 2 +- .../BrooklynMiscJacksonSerializationTest.java | 96 +++++++++++++++++++--- .../util/core/internal/TypeCoercionsTest.java | 2 +- .../util/core/predicates/DslPredicateTest.java | 12 ++- 5 files changed, 111 insertions(+), 27 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 f04e7fc6c9..c829dfed68 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 @@ -29,10 +29,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.google.common.annotations.Beta; import com.google.common.reflect.TypeToken; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; import java.util.function.Predicate; import org.apache.brooklyn.api.entity.Entity; @@ -157,9 +154,21 @@ public class BeanWithTypeUtils { * see in JsonDeserializerForCommonBrooklynThings. See DslSerializationTest. */ + static ThreadLocal<Stack<Object>> activeConversions = new ThreadLocal<>(); public static <T> T convert(ManagementContext mgmt, Object mapOrListToSerializeThenDeserialize, TypeToken<T> type, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowJavaTypes) throws JsonProcessingException { + Stack<Object> stack = activeConversions.get(); + if (stack==null) { + stack = new Stack<>(); + activeConversions.set(stack); + } + // simple things, like string, might be converted by a JsonDeserializer or by other TypeCoercions; + // so the former calls the latter, and the latter calls the former. but stop at some point! + if (stack.contains(mapOrListToSerializeThenDeserialize)) throw new IllegalStateException("Aborting recursive attempt to convert '"+mapOrListToSerializeThenDeserialize+"'"); + try { + stack.push(mapOrListToSerializeThenDeserialize); + return convertDeeply(mgmt, mapOrListToSerializeThenDeserialize, type, allowRegisteredTypes, loader, allowJavaTypes); } catch (Exception e) { @@ -168,6 +177,9 @@ public class BeanWithTypeUtils { } catch (Exception e2) { throw Exceptions.propagate(Arrays.asList(e, e2)); } + } finally { + stack.pop(); + if (stack.isEmpty()) activeConversions.remove(); } } @@ -208,7 +220,7 @@ public class BeanWithTypeUtils { if (inputMap.isAbsent()) return (Maybe<T>)inputMap; Object o = inputMap.get(); - if (!(o instanceof Map) && !(o instanceof List) && !Boxing.isPrimitiveOrBoxedObject(o)) { + if (!(o instanceof Map) && !(o instanceof List) && !Boxing.isPrimitiveOrBoxedObject(o) && !(o instanceof String)) { if (type.isSupertypeOf(o.getClass())) { return (Maybe<T>)inputMap; } else { @@ -299,7 +311,7 @@ public class BeanWithTypeUtils { } } - // we want some special object. if we have a map or a string then conversion might sort us out. - return (t instanceof Map || t instanceof String) ? 1 : -1; + // we want some special object. if we have a map or a string or possibly a primitive then conversion might sort us out. + return (t instanceof Map || t instanceof String || Boxing.isPrimitiveOrBoxedObject(t)) ? 1 : -1; } } diff --git a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java index 0e3fa09bd3..d630819a4a 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java @@ -361,7 +361,7 @@ public class TypeCoercions { @Override public <T> Maybe<T> tryCoerce(Object input, TypeToken<T> type) { - if (!(input instanceof Map || input instanceof Collection || Boxing.isPrimitiveOrBoxedObject(input))) { + if (!(input instanceof Map || input instanceof Collection || Boxing.isPrimitiveOrBoxedObject(input) || input instanceof String)) { return null; } if (BeanWithTypeUtils.isConversionRecommended(Maybe.of(input), type)) { 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 deee812b4c..5caa8ef19f 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 @@ -18,36 +18,33 @@ */ package org.apache.brooklyn.core.resolve.jackson; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.util.TokenBuffer; 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 java.util.function.BiConsumer; -import java.util.function.Consumer; -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.MutableList; import org.apache.brooklyn.util.collections.MutableMap; -import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.core.flags.TypeCoercions; import org.apache.brooklyn.util.core.units.ByteSize; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.javalang.JavaClassNames; import org.apache.brooklyn.util.text.Secret; -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; @@ -253,4 +250,81 @@ public class BrooklynMiscJacksonSerializationTest implements MapperTestFixture { WrappedValue<Secret<String>> s1; } + + @JsonDeserialize(using = SampleFromStringDeserializer.class) + static class SampleFromStringDeserialized { + public Object subtypeWanted; + public String x; + } + @JsonDeserialize(using = JsonDeserializer.None.class) + static class SampleFromStringSubtype extends SampleFromStringDeserialized {} + @JsonDeserialize(using = JsonDeserializer.None.class) + static class SampleFromStringSubtype1 extends SampleFromStringSubtype {} + static class SampleFromStringSubtype2 extends SampleFromStringSubtype {} + + static class SampleFromStringDeserializer extends JsonDeserializer { + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + TokenBuffer buffer = BrooklynJacksonSerializationUtils.createBufferForParserCurrentObject(p, ctxt); + Object raw = new JsonPassThroughDeserializer().deserialize( + BrooklynJacksonSerializationUtils.createParserFromTokenBufferAndParser(buffer, p), + ctxt); + + Integer rawi = null; + if (raw instanceof String) rawi = Integer.parseInt((String)raw); + if (raw instanceof Integer) rawi = (Integer)raw; + if (rawi!=null) { + SampleFromStringSubtype result = null; + if (rawi==1) result = new SampleFromStringSubtype1(); + if (rawi==2) result = new SampleFromStringSubtype2(); + if (result!=null) { + result.subtypeWanted = raw; + } + return result; + } + + if (raw instanceof Map) { + Integer stw = TypeCoercions.tryCoerce(((Map) raw).get("subtypeWanted"), Integer.class).orNull(); + if (stw!=null && stw>=0) { + try { + return ctxt.findNonContextualValueDeserializer(ctxt.constructType(Class.forName(SampleFromStringSubtype.class.getName()+stw))).deserialize( + BrooklynJacksonSerializationUtils.createParserFromTokenBufferAndParser(buffer, p), ctxt); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + + return ctxt.findNonContextualValueDeserializer(ctxt.constructType(SampleFromStringSubtype.class)).deserialize( + BrooklynJacksonSerializationUtils.createParserFromTokenBufferAndParser(buffer, p), ctxt); + } + } + + @Test + public void testDeserializeFromStringOrMapOrEvenWithMapKey() throws JsonProcessingException { + Object s; + mapper = BeanWithTypeUtils.newSimpleYamlMapper(); //YAMLMapper.builder().build(); + s = mapper.readerFor(SampleFromStringDeserialized.class).readValue("1"); + Asserts.assertInstanceOf(s, SampleFromStringSubtype1.class); + Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, 1 ); + + s = mapper.readerFor(SampleFromStringDeserialized.class).readValue("\"2\""); + Asserts.assertInstanceOf(s, SampleFromStringSubtype2.class); + Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, "2" ); + + s = mapper.readerFor(SampleFromStringDeserialized.class).readValue("subtypeWanted: 1"); + Asserts.assertInstanceOf(s, SampleFromStringSubtype1.class); + Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, 1 ); + + s = mapper.readerFor(SampleFromStringDeserialized.class).readValue("subtypeWanted: \"-1\""); + Asserts.assertEquals(s.getClass(), SampleFromStringSubtype.class); + Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, "-1" ); + + s = TypeCoercions.coerce("1", SampleFromStringDeserialized.class); + Asserts.assertInstanceOf(s, SampleFromStringSubtype1.class); + Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, "1" ); + s = TypeCoercions.coerce(1, SampleFromStringDeserialized.class); + Asserts.assertEquals( ((SampleFromStringSubtype)s).subtypeWanted, 1 ); + } + } diff --git a/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java b/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java index 38e40ae234..c9468f1431 100644 --- a/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java +++ b/core/src/test/java/org/apache/brooklyn/util/core/internal/TypeCoercionsTest.java @@ -386,7 +386,7 @@ public class TypeCoercionsTest { @Test public void testCoerceStringToNumber() { - assertEquals(TypeCoercions.coerce("1", Number.class), Double.valueOf(1)); + assertEquals(TypeCoercions.coerce("1", Number.class), 1); // since Jackson is permtitted to coerce, this prefers integers over doubles, 2023-05 assertEquals(TypeCoercions.coerce("1.0", Number.class), Double.valueOf(1.0)); } diff --git a/core/src/test/java/org/apache/brooklyn/util/core/predicates/DslPredicateTest.java b/core/src/test/java/org/apache/brooklyn/util/core/predicates/DslPredicateTest.java index 74bc686501..5378589e08 100644 --- a/core/src/test/java/org/apache/brooklyn/util/core/predicates/DslPredicateTest.java +++ b/core/src/test/java/org/apache/brooklyn/util/core/predicates/DslPredicateTest.java @@ -65,13 +65,11 @@ public class DslPredicateTest extends BrooklynMgmtUnitTestSupport { } @Test - public void testImplicitEqualsAsRawPredicateNotSupported() { - Asserts.assertFailsWith(() -> { - Predicate p = TypeCoercions.coerce("x", Predicate.class); - }, e -> { - Asserts.assertStringContainsIgnoreCase(e.toString(), "cannot coerce", "string", "predicate"); - return true; - }); + public void testImplicitEqualsAsRawPredicate() { + // now this is supported, 2023-05, since we can coerce strings to DslPredicates + Predicate p = TypeCoercions.coerce("x", Predicate.class); + Asserts.assertTrue(p.test("x")); + Asserts.assertFalse(p.test("y")); } DslPredicates.DslPredicate predicate(String key, Object value) {
