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


The following commit(s) were added to refs/heads/master by this push:
     new 6401e02159 allow maps expected containing a type key
6401e02159 is described below

commit 6401e02159aae9737bb80cece4f09010b0ce9ffb
Author: Alex Heneveld <[email protected]>
AuthorDate: Fri Aug 11 17:50:31 2023 +0100

    allow maps expected containing a type key
    
    previously it would instantiate the type;
    also adds a static option to permit object expected to allow that (but 
disabled so we get helpful error messages)
---
 .../resolve/jackson/AsPropertyIfAmbiguous.java     |  40 +++++++-
 .../util/core/xstream/SafeThrowableConverter.java  |   3 -
 .../brooklyn/util/core/xstream/XmlSerializer.java  |   5 +-
 ...klynRegisteredTypeJacksonSerializationTest.java | 101 ++++++++++++++++++++-
 4 files changed, 134 insertions(+), 15 deletions(-)

diff --git 
a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/AsPropertyIfAmbiguous.java
 
b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/AsPropertyIfAmbiguous.java
index 94405ce681..6824985ef2 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/AsPropertyIfAmbiguous.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/resolve/jackson/AsPropertyIfAmbiguous.java
@@ -21,7 +21,6 @@ package org.apache.brooklyn.core.resolve.jackson;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
 import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonLocation;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.JsonToken;
 import com.fasterxml.jackson.core.type.WritableTypeId;
@@ -45,7 +44,6 @@ import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.javalang.Reflections;
 import org.apache.brooklyn.util.text.Strings;
-import org.apache.commons.lang3.tuple.Pair;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -68,6 +66,11 @@ public class AsPropertyIfAmbiguous {
     /** @deprecated since 1.1 now use transform fn, and prefer wrapped in 
parens */
     public static final String CONFLICTING_TYPE_NAME_PROPERTY_PREFIX = "@";
 
+    // we can change this to false to allow e.g. deserialize "{type: unknown}" 
as a map when an object is expected;
+    // however it is probably more useful to return that as an error, because 
usually it is an error,
+    // and have a special way of permitting it in places
+    public static final boolean 
THROW_ON_OBJECT_EXPECTED_AND_INVALID_TYPE_KEY_SUPPLIED = true;
+
     public interface HasBaseType {
         JavaType getBaseType();
     }
@@ -310,6 +313,12 @@ public class AsPropertyIfAmbiguous {
         }
 
         private DiscoveredTypeAndCachedTokenBuffer 
findTypeIdOrUnambiguous(JsonParser p, DeserializationContext ctxt, JsonToken t, 
TokenBuffer tb, boolean ignoreCase, boolean mustUseConflictingTypePrefix) 
throws IOException {
+            if (baseType()!=null && 
Map.class.isAssignableFrom(baseType().getRawClass())) {
+                // if a map is expected, don't try to do fancy type lookup;
+                // we ignore subclasses of maps anyway (eg see test for 
FancyMap)
+                return new DiscoveredTypeAndCachedTokenBuffer(null, tb, false);
+            }
+
             String typeUnambiguous1 = 
CONFLICTING_TYPE_NAME_PROPERTY_TRANSFORM.apply(_typePropertyName);
             String typeUnambiguous2 = 
CONFLICTING_TYPE_NAME_PROPERTY_TRANSFORM_ALT.apply(_typePropertyName);
 
@@ -328,9 +337,30 @@ public class AsPropertyIfAmbiguous {
                     String typeId = p.getValueAsString();
                     if (typeId != null) {
                         boolean disallowed = false;
-                        if (ambiguousName) {
-                            JavaType tt = _idResolver.typeFromId(ctxt, typeId);
-                            if 
(BrooklynObject.class.isAssignableFrom(tt.getRawClass()) && 
!Feed.class.isAssignableFrom(tt.getRawClass())) {
+
+                        JavaType tt = null;
+                        try {
+                            tt = _idResolver.typeFromId(ctxt, typeId);
+                        } catch (Exception e) {
+                            Exceptions.propagateIfInterrupt(e);
+                            if 
(!THROW_ON_OBJECT_EXPECTED_AND_INVALID_TYPE_KEY_SUPPLIED && (baseType()==null 
|| hasTypePropertyNameAsField(baseType()) || 
baseType().getRawClass().isAssignableFrom(Map.class))) {
+                                // if we allow an object with a type key here, 
don't throw
+
+                                // 2023-08 previously we would throw here if 
it was an ambiguous name; if an unambiguous name we didn't check the type,
+                                // and it would throw later, but throwing now 
is fine (if we want to throw)
+
+                            } else {
+                                throw Exceptions.propagate(e);
+                            }
+                            // incompatible type, or type not found; just 
ignore for now
+                            // (although the error might be useful)
+                            tt = null;
+                            disallowed = true;
+                        }
+
+                        if (ambiguousName && !disallowed) {
+
+                            if (tt!=null && 
BrooklynObject.class.isAssignableFrom(tt.getRawClass()) && 
!Feed.class.isAssignableFrom(tt.getRawClass())) {
                                 Boolean wantsSpec = null;
                                 Boolean wantsBO = null;
 
diff --git 
a/core/src/main/java/org/apache/brooklyn/util/core/xstream/SafeThrowableConverter.java
 
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/SafeThrowableConverter.java
index fc3c84cad8..9097b15869 100644
--- 
a/core/src/main/java/org/apache/brooklyn/util/core/xstream/SafeThrowableConverter.java
+++ 
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/SafeThrowableConverter.java
@@ -23,7 +23,6 @@ import com.thoughtworks.xstream.converters.Converter;
 import com.thoughtworks.xstream.converters.MarshallingContext;
 import com.thoughtworks.xstream.converters.UnmarshallingContext;
 import com.thoughtworks.xstream.core.DefaultConverterLookup;
-import com.thoughtworks.xstream.core.util.QuickWriter;
 import com.thoughtworks.xstream.io.HierarchicalStreamReader;
 import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
 import com.thoughtworks.xstream.io.path.PathTrackingWriter;
@@ -33,8 +32,6 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.net.Inet4Address;
-import java.net.UnknownHostException;
 import java.util.function.Predicate;
 
 /** This is a hacky way to try to recover in some non-serializable situations. 
But it can and often does
diff --git 
a/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java 
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java
index 2484a2c6a3..d7b19cb27f 100644
--- 
a/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java
+++ 
b/core/src/main/java/org/apache/brooklyn/util/core/xstream/XmlSerializer.java
@@ -27,9 +27,7 @@ import com.google.common.collect.Maps;
 import com.thoughtworks.xstream.XStream;
 import com.thoughtworks.xstream.XStreamException;
 import com.thoughtworks.xstream.converters.extended.JavaClassConverter;
-import com.thoughtworks.xstream.core.ClassLoaderReference;
 import com.thoughtworks.xstream.core.DefaultConverterLookup;
-import com.thoughtworks.xstream.core.util.CompositeClassLoader;
 import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
 import com.thoughtworks.xstream.io.naming.NameCoder;
 import com.thoughtworks.xstream.io.path.PathTracker;
@@ -89,7 +87,8 @@ public class XmlSerializer<T> {
 
         converterLookup = new DefaultConverterLookup();
 
-        xstream = new XStream(null, hierarchicalStreamDriver, new 
ClassLoaderReference(new CompositeClassLoader()), (Mapper)null,
+        XStream xs1 = new XStream(); // use this to get the class loader 
because its package isn't exposed
+        xstream = new XStream(null, hierarchicalStreamDriver, 
xs1.getClassLoaderReference(), (Mapper)null,
                 type ->  converterLookup.lookupConverterForType(type),
                 (converter,priority) -> 
converterLookup.registerConverter(converter, priority)
         ) {
diff --git 
a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynRegisteredTypeJacksonSerializationTest.java
 
b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynRegisteredTypeJacksonSerializationTest.java
index 840371893b..8b5c03188b 100644
--- 
a/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynRegisteredTypeJacksonSerializationTest.java
+++ 
b/core/src/test/java/org/apache/brooklyn/core/resolve/jackson/BrooklynRegisteredTypeJacksonSerializationTest.java
@@ -27,10 +27,7 @@ import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.sensor.StaticSensor;
 import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
 import org.apache.brooklyn.core.test.BrooklynMgmtUnitTestSupport;
-import org.apache.brooklyn.core.typereg.BasicBrooklynTypeRegistry;
 import org.apache.brooklyn.core.typereg.BasicTypeImplementationPlan;
-import org.apache.brooklyn.core.typereg.JavaClassNameTypePlanTransformer;
-import org.apache.brooklyn.core.typereg.RegisteredTypes;
 import org.apache.brooklyn.core.workflow.WorkflowSensor;
 import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.util.collections.MutableList;
@@ -42,7 +39,9 @@ import org.apache.brooklyn.util.time.Duration;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Objects;
 
 public class BrooklynRegisteredTypeJacksonSerializationTest extends 
BrooklynMgmtUnitTestSupport implements MapperTestFixture {
 
@@ -76,7 +75,12 @@ public class BrooklynRegisteredTypeJacksonSerializationTest 
extends BrooklynMgmt
     public void testDeserializeUnknownTypeFails() throws 
JsonProcessingException {
         try {
             Object x = BeanWithTypeUtils.newYamlMapper(mgmt, true, null, 
true).readValue("type: DeliberatelyMissing", Object.class);
-            Asserts.shouldHaveFailedPreviously("Should have failed due to 
unknown type; instead got "+x);
+            if 
(AsPropertyIfAmbiguous.THROW_ON_OBJECT_EXPECTED_AND_INVALID_TYPE_KEY_SUPPLIED) {
+                Asserts.shouldHaveFailedPreviously("Should have failed due to 
unknown type; instead got " + x);
+            } else {
+                Asserts.assertInstanceOf(x, Map.class);
+                Asserts.assertSize((Map)x, 1);
+            }
         } catch (Exception e) {
             Asserts.expectedFailureContains(e, "DeliberatelyMissing");
         }
@@ -232,6 +236,95 @@ public class 
BrooklynRegisteredTypeJacksonSerializationTest extends BrooklynMgmt
         Asserts.assertEquals(redeserObj.x, "hello");
     }
 
+    static class FancyHolder {
+        Map map;
+        Object obj;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            FancyHolder mapHolder = (FancyHolder) o;
+            return Objects.equals(map, mapHolder.map) && Objects.equals(obj, 
mapHolder.obj);
+        }
+    }
+
+    static class FancyMap extends LinkedHashMap {
+    }
+
+    @Test
+    public void testSerializeAndDesMapWithTypeEntry() throws Exception {
+        // if a map is _expected_ at least we should ignore the field type
+        FancyHolder a = new FancyHolder();
+        a.map = MutableMap.of("type", "not_a_type");
+
+        {
+            String expectedSerialization = 
"{\"map\":{\"type\":\"not_a_type\"}}";
+            Assert.assertEquals(ser(a, FancyHolder.class), 
expectedSerialization);
+            Assert.assertEquals(deser(expectedSerialization, 
FancyHolder.class), a);
+        }
+
+        {
+            FancyMap f = new FancyMap();
+            f.put("a",1);
+            // types ignored when serializing any subclass of map; we could 
change if we want (but make MutableMap the default)
+            Assert.assertEquals(ser(f), "{\"a\":1}");
+        }
+
+        {
+            String expectedDeserialization = "{\"type\":\""+ 
BrooklynRegisteredTypeJacksonSerializationTest.FancyHolder.class.getName() 
+"\"," +
+                    "\"map\":{\"type\":\"not_a_type\"}}";
+            Assert.assertEquals(ser(a), expectedDeserialization);
+            Assert.assertEquals(deser(expectedDeserialization), a);
+        }
+
+        a.map = null;
+        a.obj = MutableMap.of("type", 
BrooklynRegisteredTypeJacksonSerializationTest.FancyHolder.class.getName());
+        {
+            // it is expected that a map containing a type comes back as an 
instance of that type if possible
+            String expectedSerialization = "{\"obj\":{\"type\":\"" + 
BrooklynRegisteredTypeJacksonSerializationTest.FancyHolder.class.getName() + 
"\"}}";
+            Assert.assertEquals(ser(a, FancyHolder.class), 
expectedSerialization);
+            Asserts.assertThat(deser(expectedSerialization, 
FancyHolder.class).obj, r -> r instanceof FancyHolder);
+
+            // an unknown field should undo that
+            ((Map) a.obj).put("unfield", "should_force_map");
+            Asserts.assertThat(deser(ser(a, FancyHolder.class), 
FancyHolder.class), r -> {
+                Asserts.assertInstanceOf(r.obj, Map.class);
+                Asserts.assertEquals(((Map) r.obj).get("type"), 
BrooklynRegisteredTypeJacksonSerializationTest.FancyHolder.class.getName());
+                Asserts.assertEquals(((Map) r.obj).get("unfield"), 
"should_force_map");
+                return true;
+            });
+        }
+
+        if 
(!AsPropertyIfAmbiguous.THROW_ON_OBJECT_EXPECTED_AND_INVALID_TYPE_KEY_SUPPLIED) 
{
+            // all the below will throw if the above is true;
+            // see other tests that reference the constant above
+
+            // and an unknown field also forces a map
+            ((Map) a.obj).put("type", "not_a_type");
+            Asserts.assertThat(deser(ser(a, FancyHolder.class), 
FancyHolder.class), r -> {
+                Asserts.assertInstanceOf(r.obj, Map.class);
+                Asserts.assertEquals(((Map) r.obj).get("type"), "not_a_type");
+                Asserts.assertEquals(((Map) r.obj).get("unfield"), 
"should_force_map");
+                return true;
+            });
+            // also with unambiguous name
+            ((Map) a.obj).put("@type", "not_a_type");
+            Asserts.assertThat(deser(ser(a, FancyHolder.class), 
FancyHolder.class), r -> {
+                Asserts.assertInstanceOf(r.obj, Map.class);
+                Asserts.assertEquals(((Map) r.obj).get("type"), "not_a_type");
+                Asserts.assertEquals(((Map) r.obj).get("unfield"), 
"should_force_map");
+                return true;
+            });
+
+            // what if it's an Object where we don't know the type? probably 
we should throw, we always have in the past, but we could allow
+            Map b = MutableMap.of("type", "not_a_type");
+            Asserts.assertEquals(ser(b), "{\"type\":\"not_a_type\"}");
+            Asserts.assertEquals(deser(ser(b)), MutableMap.of("type", 
"not_a_type"));
+        }
+
+    }
+
     @Test
     public void testDeserializeEntityInitializerWithTypeField() throws 
Exception {
         BrooklynAppUnitTestSupport.addRegisteredTypeBean(mgmt, 
"samplebean-with-type", "1", SampleBeanWithType.class);

Reply via email to