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) {

Reply via email to