This is an automated email from the ASF dual-hosted git repository.

cwylie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 8718155  Allow for empty keys in hash map (#10869)
8718155 is described below

commit 8718155f8ffa8c9d34877cbdb7f8eb86df383e29
Author: Abhishek Agarwal <[email protected]>
AuthorDate: Thu Feb 11 00:49:57 2021 +0530

    Allow for empty keys in hash map (#10869)
    
    * allow for empty keys in hash map
    
    * fix serde test
---
 .../druid/guice/GuiceAnnotationIntrospector.java   |  19 ++-
 .../druid/guice/DruidSecondaryModuleTest.java      | 157 ++++++++++++++++++++-
 .../extraction/MapLookupExtractionFnSerDeTest.java |  13 +-
 3 files changed, 180 insertions(+), 9 deletions(-)

diff --git 
a/core/src/main/java/org/apache/druid/guice/GuiceAnnotationIntrospector.java 
b/core/src/main/java/org/apache/druid/guice/GuiceAnnotationIntrospector.java
index 29fac1a..ce49253 100644
--- a/core/src/main/java/org/apache/druid/guice/GuiceAnnotationIntrospector.java
+++ b/core/src/main/java/org/apache/druid/guice/GuiceAnnotationIntrospector.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JacksonInject;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.introspect.Annotated;
+import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
 import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
 import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
 import com.fasterxml.jackson.databind.introspect.AnnotatedParameter;
@@ -32,6 +33,7 @@ import com.google.inject.Key;
 import org.apache.druid.java.util.common.IAE;
 
 import java.lang.annotation.Annotation;
+import java.util.Map;
 
 /**
  */
@@ -101,6 +103,7 @@ public class GuiceAnnotationIntrospector extends 
NopAnnotationIntrospector
     // delegate creators. I'm not 100% sure why it's not called, but guess 
it's because the argument
     // is some Java type that Jackson already knows how to deserialize. Since 
there is only one argument,
     // Jackson perhaps is able to just deserialize it without introspection.
+
     if (ac instanceof AnnotatedParameter) {
       final AnnotatedParameter ap = (AnnotatedParameter) ac;
       if (ap.hasAnnotation(JsonProperty.class)) {
@@ -108,6 +111,20 @@ public class GuiceAnnotationIntrospector extends 
NopAnnotationIntrospector
       }
     }
 
-    return JsonIgnoreProperties.Value.forIgnoredProperties("");
+    // A map can have empty string keys e.g. 
https://github.com/apache/druid/issues/10859. By returning empty ignored
+    // list for map, we can allow for empty string keys in a map. A nested 
class within map
+    // can still be annotated with JacksonInject and still be 
non-deserializable from user input
+    // Refer to {@link 
com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.createMapDeserializer}
 for details
+    // on how the ignored list is passed to map deserializer
+    if (ac instanceof AnnotatedClass) {
+      final AnnotatedClass aClass = (AnnotatedClass) ac;
+      if (Map.class.isAssignableFrom(aClass.getAnnotated())) {
+        return JsonIgnoreProperties.Value.empty();
+      }
+    }
+
+    // We will allow serialization on empty properties. Properties marked with 
{@code @JacksonInject} are still
+    // not serialized if there is no getter marked with {@code @JsonProperty}
+    return 
JsonIgnoreProperties.Value.forIgnoredProperties("").withAllowGetters();
   }
 }
diff --git 
a/core/src/test/java/org/apache/druid/guice/DruidSecondaryModuleTest.java 
b/core/src/test/java/org/apache/druid/guice/DruidSecondaryModuleTest.java
index 12ab2dd..ddda5a5 100644
--- a/core/src/test/java/org/apache/druid/guice/DruidSecondaryModuleTest.java
+++ b/core/src/test/java/org/apache/druid/guice/DruidSecondaryModuleTest.java
@@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonValue;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Module;
@@ -38,6 +39,8 @@ import javax.annotation.Nullable;
 import javax.validation.Validation;
 import javax.validation.Validator;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.Properties;
 
 @RunWith(Enclosed.class)
@@ -90,6 +93,89 @@ public class DruidSecondaryModuleTest
       Assert.assertEquals(PROPERTY_VALUE, object.injected.val);
     }
 
+    @Test
+    public void testInjectNormalWithEmptyKeysInMap() throws 
JsonProcessingException
+    {
+      final Properties props = new Properties();
+      props.setProperty(PROPERTY_NAME, PROPERTY_VALUE);
+      final Injector injector = makeInjectorWithProperties(props);
+      final ObjectMapper mapper = makeObjectMapper(injector);
+      final String json = "{\n"
+                          + "  \"map1\" : {\n"
+                          + "    \"foo\" : \"bar\",\n"
+                          + "    \"\" : \"empty\"\n"
+                          + "  },\n"
+                          + "  \"map2\" : {\n"
+                          + "    \"foo\" : {\n"
+                          + "      \"test\" : \"value1\"\n"
+                          + "    },\n"
+                          + "    \"\" : {\n"
+                          + "      \"test\" : \"value2\"\n"
+                          + "    }\n"
+                          + "  }\n"
+                          + "}";
+      final ClassWithMapAndJacksonInject object = new 
ClassWithMapAndJacksonInject(
+          ImmutableMap.of("foo", "bar", "", "empty"),
+          ImmutableMap.of("foo", new ClassWithJacksonInject("value1", 
injector.getInstance(InjectedParameter.class)),
+          "", new ClassWithJacksonInject("value2", 
injector.getInstance(InjectedParameter.class)))
+      );
+      final String jsonWritten = 
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
+      Assert.assertEquals(json, jsonWritten);
+      final ClassWithMapAndJacksonInject objectRead = mapper.readValue(json, 
ClassWithMapAndJacksonInject.class);
+      Assert.assertEquals(object, objectRead);
+      Assert.assertEquals("empty", objectRead.getStringStringMap().get(""));
+    }
+
+    @Test
+    public void testInjectNormalWithEmptyKeysAndInjectClass() throws 
JsonProcessingException
+    {
+      final Properties props = new Properties();
+      props.setProperty(PROPERTY_NAME, PROPERTY_VALUE);
+      final Injector injector = makeInjectorWithProperties(props);
+      final ObjectMapper mapper = makeObjectMapper(injector);
+      final String json = "{\n"
+                          + "  \"map1\" : {\n"
+                          + "    \"foo\" : \"bar\",\n"
+                          + "    \"\" : \"empty\"\n"
+                          + "  },\n"
+                          + "  \"map2\" : {\n"
+                          + "    \"foo\" : {\n"
+                          + "      \"test\" : \"value1\",\n"
+                          + "      \"\"     : \"nice try\"\n"
+                          + "    },\n"
+                          + "    \"\" : {\n"
+                          + "      \"test\" : \"value2\",\n"
+                          + "      \"\"     : \"nice try\"\n"
+                          + "    }\n"
+                          + "  }\n"
+                          + "}";
+      final String expectedSerializedJson
+          = "{\n"
+            + "  \"map1\" : {\n"
+            + "    \"foo\" : \"bar\",\n"
+            + "    \"\" : \"empty\"\n"
+            + "  },\n"
+            + "  \"map2\" : {\n"
+            + "    \"foo\" : {\n"
+            + "      \"test\" : \"value1\"\n"
+            + "    },\n"
+            + "    \"\" : {\n"
+            + "      \"test\" : \"value2\"\n"
+            + "    }\n"
+            + "  }\n"
+            + "}";
+      final ClassWithMapAndJacksonInject object = new 
ClassWithMapAndJacksonInject(
+          ImmutableMap.of("foo", "bar", "", "empty"),
+          ImmutableMap.of("foo", new ClassWithJacksonInject("value1", 
injector.getInstance(InjectedParameter.class)),
+                          "", new ClassWithJacksonInject("value2", 
injector.getInstance(InjectedParameter.class)))
+      );
+      final String jsonWritten = 
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
+      Assert.assertEquals(expectedSerializedJson, jsonWritten);
+      final ClassWithMapAndJacksonInject objectRead = mapper.readValue(json, 
ClassWithMapAndJacksonInject.class);
+      Assert.assertEquals(object, objectRead);
+      Assert.assertEquals("empty", objectRead.getStringStringMap().get(""));
+    }
+
     private static class ClassWithJacksonInject
     {
       private final String test;
@@ -106,11 +192,30 @@ public class DruidSecondaryModuleTest
         this.injected = injected;
       }
 
-      @JsonProperty
+      @JsonProperty("test")
       public String getTest()
       {
         return test;
       }
+
+      @Override
+      public boolean equals(Object o)
+      {
+        if (this == o) {
+          return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+          return false;
+        }
+        ClassWithJacksonInject that = (ClassWithJacksonInject) o;
+        return Objects.equals(test, that.test) && Objects.equals(injected.val, 
that.injected.val);
+      }
+
+      @Override
+      public int hashCode()
+      {
+        return Objects.hash(test, injected.val);
+      }
     }
 
     private static class ClassWithEmptyProperty
@@ -135,6 +240,56 @@ public class DruidSecondaryModuleTest
         return test;
       }
     }
+
+    private static class ClassWithMapAndJacksonInject
+    {
+      private final Map<String, String> stringStringMap;
+      private final Map<String, ClassWithJacksonInject> stringJacksonInjectMap;
+
+      @JsonCreator
+      public ClassWithMapAndJacksonInject(
+          @JsonProperty("map1") Map<String, String> stringStringMap,
+          @JsonProperty("map2") Map<String, ClassWithJacksonInject> 
stringJacksonInjectMap
+      )
+      {
+        this.stringStringMap = stringStringMap;
+        this.stringJacksonInjectMap = stringJacksonInjectMap;
+      }
+
+      @JsonProperty("map1")
+      public Map<String, String> getStringStringMap()
+      {
+        return stringStringMap;
+      }
+
+      @JsonProperty("map2")
+      public Map<String, ClassWithJacksonInject> getStringJacksonInjectMap()
+      {
+        return stringJacksonInjectMap;
+      }
+
+      @Override
+      public boolean equals(Object o)
+      {
+        if (this == o) {
+          return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+          return false;
+        }
+        ClassWithMapAndJacksonInject that = (ClassWithMapAndJacksonInject) o;
+        return Objects.equals(stringStringMap, that.stringStringMap) && 
Objects.equals(
+            stringJacksonInjectMap,
+            that.stringJacksonInjectMap
+        );
+      }
+
+      @Override
+      public int hashCode()
+      {
+        return Objects.hash(stringStringMap, stringJacksonInjectMap);
+      }
+    }
   }
 
   public static class ConstructorWithoutJacksonInjectTest
diff --git 
a/processing/src/test/java/org/apache/druid/query/extraction/MapLookupExtractionFnSerDeTest.java
 
b/processing/src/test/java/org/apache/druid/query/extraction/MapLookupExtractionFnSerDeTest.java
index 92e5ef7..39144ff 100644
--- 
a/processing/src/test/java/org/apache/druid/query/extraction/MapLookupExtractionFnSerDeTest.java
+++ 
b/processing/src/test/java/org/apache/druid/query/extraction/MapLookupExtractionFnSerDeTest.java
@@ -21,11 +21,9 @@ package org.apache.druid.query.extraction;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.ImmutableMap;
-import com.google.inject.Injector;
-import com.google.inject.Key;
-import org.apache.druid.guice.GuiceInjectors;
-import org.apache.druid.guice.annotations.Json;
+import org.apache.druid.common.config.NullHandling;
 import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.segment.TestHelper;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -42,19 +40,20 @@ public class MapLookupExtractionFnSerDeTest
   private static ObjectMapper mapper;
   private static final Map<String, String> RENAMES = ImmutableMap.of(
       "foo", "bar",
-      "bar", "baz"
+      "bar", "baz",
+      "", "empty"
   );
 
   @BeforeClass
   public static void setup()
   {
-    Injector defaultInjector = GuiceInjectors.makeStartupInjector();
-    mapper = defaultInjector.getInstance(Key.get(ObjectMapper.class, 
Json.class));
+    mapper = TestHelper.makeJsonMapper();
   }
 
   @Test
   public void testDeserialization() throws IOException
   {
+    NullHandling.initializeForTests();
     final DimExtractionFn fn = 
mapper.readerFor(DimExtractionFn.class).readValue(
         StringUtils.format(
             "{\"type\":\"lookup\",\"lookup\":{\"type\":\"map\", \"map\":%s}}",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to