mirror of https://github.com/apache/druid.git
Allow for empty keys in hash map (#10869)
* allow for empty keys in hash map * fix serde test
This commit is contained in:
parent
1ec3f0bd73
commit
8718155f8f
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}}",
|
||||
|
|
Loading…
Reference in New Issue