From 3f8f00a231c4b47981e0e2e48f18f1221249e6f3 Mon Sep 17 00:00:00 2001 From: Jihoon Son Date: Thu, 4 Feb 2021 11:21:43 -0800 Subject: [PATCH] Fix CVE-2021-25646 (#10818) --- .../druid/guice/DruidSecondaryModule.java | 26 +- .../guice/GuiceAnnotationIntrospector.java | 54 +++ .../input/impl/DimensionsSpecSerdeTest.java | 5 + .../input/impl/StringDimensionSchemaTest.java | 63 ++++ .../druid/guice/DruidSecondaryModuleTest.java | 353 ++++++++++++++++++ .../druid/indexing/common/TestUtils.java | 3 + .../org/apache/druid/segment/TestHelper.java | 22 ++ 7 files changed, 521 insertions(+), 5 deletions(-) create mode 100644 core/src/test/java/org/apache/druid/data/input/impl/StringDimensionSchemaTest.java create mode 100644 core/src/test/java/org/apache/druid/guice/DruidSecondaryModuleTest.java diff --git a/core/src/main/java/org/apache/druid/guice/DruidSecondaryModule.java b/core/src/main/java/org/apache/druid/guice/DruidSecondaryModule.java index 319c0d1f1dc..5e4db78ac82 100644 --- a/core/src/main/java/org/apache/druid/guice/DruidSecondaryModule.java +++ b/core/src/main/java/org/apache/druid/guice/DruidSecondaryModule.java @@ -19,8 +19,10 @@ package org.apache.druid.guice; +import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair; +import com.google.common.annotations.VisibleForTesting; import com.google.inject.Binder; import com.google.inject.Inject; import com.google.inject.Injector; @@ -83,14 +85,28 @@ public class DruidSecondaryModule implements Module return smileMapper; } - private void setupJackson(Injector injector, final ObjectMapper mapper) + @VisibleForTesting + public static void setupJackson(Injector injector, final ObjectMapper mapper) { - final GuiceAnnotationIntrospector guiceIntrospector = new GuiceAnnotationIntrospector(); - mapper.setInjectableValues(new GuiceInjectableValues(injector)); + setupAnnotationIntrospector(mapper, new GuiceAnnotationIntrospector()); + } + + @VisibleForTesting + public static void setupAnnotationIntrospector( + final ObjectMapper mapper, + final AnnotationIntrospector annotationIntrospector + ) + { mapper.setAnnotationIntrospectors( - new AnnotationIntrospectorPair(guiceIntrospector, mapper.getSerializationConfig().getAnnotationIntrospector()), - new AnnotationIntrospectorPair(guiceIntrospector, mapper.getDeserializationConfig().getAnnotationIntrospector()) + new AnnotationIntrospectorPair( + annotationIntrospector, + mapper.getSerializationConfig().getAnnotationIntrospector() + ), + new AnnotationIntrospectorPair( + annotationIntrospector, + mapper.getDeserializationConfig().getAnnotationIntrospector() + ) ); } } 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 7862c977ee2..29fac1ae3be 100644 --- a/core/src/main/java/org/apache/druid/guice/GuiceAnnotationIntrospector.java +++ b/core/src/main/java/org/apache/druid/guice/GuiceAnnotationIntrospector.java @@ -20,8 +20,12 @@ package org.apache.druid.guice; 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.AnnotatedMember; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.introspect.AnnotatedParameter; import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector; import com.google.inject.BindingAnnotation; import com.google.inject.Key; @@ -56,4 +60,54 @@ public class GuiceAnnotationIntrospector extends NopAnnotationIntrospector } return Key.get(m.getGenericType(), guiceAnnotation); } + + /** + * This method is used to find what property to ignore in deserialization. Jackson calls this method + * per every class and every constructor parameter. + * + * This implementation returns a {@link JsonIgnoreProperties.Value#empty()} that allows empty names if + * the parameters has the {@link JsonProperty} annotation. Otherwise, it returns + * {@code JsonIgnoreProperties.Value.forIgnoredProperties("")} that does NOT allow empty names. + * This behavior is to work around a bug in Jackson deserializer (see the below comment for details) and + * can be removed in the future after the bug is fixed. + * For example, suppose a constructor like below: + * + *
{@code
+   * @JsonCreator
+   * public ClassWithJacksonInject(
+   *   @JsonProperty("val") String val,
+   *   @JacksonInject InjectedParameter injected
+   * )
+   * }
+ * + * During deserializing a JSON string into this class, this method will be called at least twice, + * one for {@code val} and another for {@code injected}. It will return {@code Value.empty()} for {@code val}, + * while {Value.forIgnoredProperties("")} for {@code injected} because the later does not have {@code JsonProperty}. + * As a result, {@code injected} will be ignored during deserialization since it has no name. + */ + @Override + public JsonIgnoreProperties.Value findPropertyIgnorals(Annotated ac) + { + // We should not allow empty names in any case. However, there is a known bug in Jackson deserializer + // with ignorals (_arrayDelegateDeserializer is not copied when creating a contextual deserializer. + // See https://github.com/FasterXML/jackson-databind/issues/3022 for more details), which makes array + // deserialization failed even when the array is a valid field. To work around this bug, we return + // an empty ignoral when the given Annotated is a parameter with JsonProperty that needs to be deserialized. + // This is valid because every property with JsonProperty annoation should have a non-empty name. + // We can simply remove the below check after the Jackson bug is fixed. + // + // This check should be fine for so-called delegate creators that have only one argument without + // JsonProperty annotation, because this method is not even called for the argument of + // 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)) { + return JsonIgnoreProperties.Value.empty(); + } + } + + return JsonIgnoreProperties.Value.forIgnoredProperties(""); + } } diff --git a/core/src/test/java/org/apache/druid/data/input/impl/DimensionsSpecSerdeTest.java b/core/src/test/java/org/apache/druid/data/input/impl/DimensionsSpecSerdeTest.java index 45bb9156f50..9b9b90adea6 100644 --- a/core/src/test/java/org/apache/druid/data/input/impl/DimensionsSpecSerdeTest.java +++ b/core/src/test/java/org/apache/druid/data/input/impl/DimensionsSpecSerdeTest.java @@ -19,9 +19,12 @@ package org.apache.druid.data.input.impl; +import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import junit.framework.Assert; +import org.apache.druid.guice.DruidSecondaryModule; +import org.apache.druid.guice.GuiceAnnotationIntrospector; import org.junit.Test; import java.util.Arrays; @@ -34,6 +37,8 @@ public class DimensionsSpecSerdeTest private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); static { + AnnotationIntrospector introspector = new GuiceAnnotationIntrospector(); + DruidSecondaryModule.setupAnnotationIntrospector(OBJECT_MAPPER, introspector); OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } diff --git a/core/src/test/java/org/apache/druid/data/input/impl/StringDimensionSchemaTest.java b/core/src/test/java/org/apache/druid/data/input/impl/StringDimensionSchemaTest.java new file mode 100644 index 00000000000..ad2909722d2 --- /dev/null +++ b/core/src/test/java/org/apache/druid/data/input/impl/StringDimensionSchemaTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.data.input.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.druid.data.input.impl.DimensionSchema.MultiValueHandling; +import org.apache.druid.guice.DruidSecondaryModule; +import org.apache.druid.guice.GuiceAnnotationIntrospector; +import org.junit.Assert; +import org.junit.Test; + +public class StringDimensionSchemaTest +{ + private final ObjectMapper jsonMapper; + + public StringDimensionSchemaTest() + { + jsonMapper = new ObjectMapper(); + AnnotationIntrospector introspector = new GuiceAnnotationIntrospector(); + DruidSecondaryModule.setupAnnotationIntrospector(jsonMapper, introspector); + jsonMapper.registerSubtypes(StringDimensionSchema.class); + } + + @Test + public void testDeserializeFromSimpleString() throws JsonProcessingException + { + final String json = "\"dim\""; + final StringDimensionSchema schema = (StringDimensionSchema) jsonMapper.readValue(json, DimensionSchema.class); + Assert.assertEquals(new StringDimensionSchema("dim"), schema); + } + + @Test + public void testDeserializeFromJson() throws JsonProcessingException + { + final String json = "{\n" + + " \"type\" : \"StringDimensionSchema\",\n" + + " \"name\" : \"dim\",\n" + + " \"multiValueHandling\" : \"SORTED_SET\",\n" + + " \"createBitmapIndex\" : false\n" + + "}"; + final StringDimensionSchema schema = (StringDimensionSchema) jsonMapper.readValue(json, DimensionSchema.class); + Assert.assertEquals(new StringDimensionSchema("dim", MultiValueHandling.SORTED_SET, false), schema); + } +} diff --git a/core/src/test/java/org/apache/druid/guice/DruidSecondaryModuleTest.java b/core/src/test/java/org/apache/druid/guice/DruidSecondaryModuleTest.java new file mode 100644 index 00000000000..12ab2ddf027 --- /dev/null +++ b/core/src/test/java/org/apache/druid/guice/DruidSecondaryModuleTest.java @@ -0,0 +1,353 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.guice; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +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.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import org.junit.Assert; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + +import javax.annotation.Nullable; +import javax.validation.Validation; +import javax.validation.Validator; +import java.util.List; +import java.util.Properties; + +@RunWith(Enclosed.class) +public class DruidSecondaryModuleTest +{ + private static final String PROPERTY_NAME = "druid.injected.val"; + private static final String PROPERTY_VALUE = "this is the legit val"; + + public static class ConstructorWithJacksonInjectTest + { + @Test + public void testInjectWithAnEmptyPropertyNotOverrideInjection() 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 = "{\"test\": \"this is an injection test\", \"\": \"nice try\" }"; + final ClassWithJacksonInject object = mapper.readValue(json, ClassWithJacksonInject.class); + Assert.assertEquals("this is an injection test", object.test); + Assert.assertEquals(PROPERTY_VALUE, object.injected.val); + } + + @Test + public void testInjectNormal() 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 = "{\"test\": \"this is an injection test\" }"; + final ClassWithJacksonInject object = mapper.readValue(json, ClassWithJacksonInject.class); + Assert.assertEquals("this is an injection test", object.test); + Assert.assertEquals(PROPERTY_VALUE, object.injected.val); + } + + @Test + public void testInjectClassWithEmptyProperty() 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 = "{\"test\": \"this is an injection test\", \"\": \"nice try\" }"; + final ClassWithEmptyProperty object = mapper.readValue(json, ClassWithEmptyProperty.class); + Assert.assertEquals("this is an injection test", object.test); + Assert.assertEquals(PROPERTY_VALUE, object.injected.val); + } + + private static class ClassWithJacksonInject + { + private final String test; + + private InjectedParameter injected; + + @JsonCreator + public ClassWithJacksonInject( + @JsonProperty("test") String test, + @JacksonInject InjectedParameter injected + ) + { + this.test = test; + this.injected = injected; + } + + @JsonProperty + public String getTest() + { + return test; + } + } + + private static class ClassWithEmptyProperty + { + private final String test; + + private InjectedParameter injected; + + @JsonCreator + public ClassWithEmptyProperty( + @JsonProperty("test") String test, + @JacksonInject @JsonProperty("") InjectedParameter injected + ) + { + this.test = test; + this.injected = injected; + } + + @JsonProperty + public String getTest() + { + return test; + } + } + } + + public static class ConstructorWithoutJacksonInjectTest + { + @Test + public void testInjectionWithEmptyPropertyName() throws JsonProcessingException + { + final Properties props = new Properties(); + + final Injector injector = makeInjectorWithProperties(props); + final ObjectMapper mapper = makeObjectMapper(injector); + final String json = "[\"this is\", \"an injection test\"]"; + final ClassWithConstructorOfEmptyName object = mapper.readValue(json, ClassWithConstructorOfEmptyName.class); + Assert.assertEquals(ImmutableList.of("this is", "an injection test"), object.getTest()); + } + + @Test + public void testInjectEmptyListWithEmptyPropertyName() throws JsonProcessingException + { + final Properties props = new Properties(); + + final Injector injector = makeInjectorWithProperties(props); + final ObjectMapper mapper = makeObjectMapper(injector); + final String json = "[]"; + final ClassWithConstructorOfEmptyName object = mapper.readValue(json, ClassWithConstructorOfEmptyName.class); + Assert.assertEquals(ImmutableList.of(), object.getTest()); + } + + @Test + public void testInjectClassWithFactoryMethodOfEmptyPropertyName() throws JsonProcessingException + { + final Properties props = new Properties(); + + final Injector injector = makeInjectorWithProperties(props); + final ObjectMapper mapper = makeObjectMapper(injector); + final String json = "[\"this is\", \"an injection test\"]"; + final ClassWithFactoryMethodOfEmptyName object = mapper.readValue(json, ClassWithFactoryMethodOfEmptyName.class); + Assert.assertEquals(ImmutableList.of("this is", "an injection test"), object.getTest()); + } + + @Test + public void testInjectEmptyListToClassWithFactoryMethodOfEmptyPropertyName() throws JsonProcessingException + { + final Properties props = new Properties(); + + final Injector injector = makeInjectorWithProperties(props); + final ObjectMapper mapper = makeObjectMapper(injector); + final String json = "[]"; + final ClassWithFactoryMethodOfEmptyName object = mapper.readValue(json, ClassWithFactoryMethodOfEmptyName.class); + Assert.assertEquals(ImmutableList.of(), object.getTest()); + } + + @Test + public void testInjectClassOfEmptyConstructor() 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 = "{}"; + final ClassOfEmptyConstructor object = mapper.readValue(json, ClassOfEmptyConstructor.class); + Assert.assertEquals("empty constructor", object.val); + } + + private static class ClassWithConstructorOfEmptyName + { + private final List test; + + @JsonCreator + public ClassWithConstructorOfEmptyName(List test) + { + this.test = test; + } + + @JsonValue + public List getTest() + { + return test; + } + } + + private static class ClassWithFactoryMethodOfEmptyName + { + private final List test; + + @JsonCreator + public static ClassWithFactoryMethodOfEmptyName create(List test) + { + return new ClassWithFactoryMethodOfEmptyName(test); + } + + private ClassWithFactoryMethodOfEmptyName(List test) + { + this.test = test; + } + + @JsonValue + public List getTest() + { + return test; + } + } + + private static class ClassOfEmptyConstructor + { + private final String val; + + @JsonCreator + public ClassOfEmptyConstructor() + { + this.val = "empty constructor"; + } + } + } + + public static class ClassOfMultipleJsonCreatorsTest + { + @Test + public void testDeserializeUsingMultiArgumentsConstructor() 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 = "{\"val\": \"this is an injection test\", \"valLen\": 5, \"\": \"nice try\" }"; + final ClassOfMultipleJsonCreators object = mapper.readValue(json, ClassOfMultipleJsonCreators.class); + Assert.assertEquals("this is an injection test", object.val); + Assert.assertEquals(5, object.valLen); + Assert.assertNotNull(object.injected); + Assert.assertEquals(PROPERTY_VALUE, object.injected.val); + } + + @Test + public void testDeserializeUsingDelegateConstructor() 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 = "\"this is an injection test\""; + final ClassOfMultipleJsonCreators object = mapper.readValue(json, ClassOfMultipleJsonCreators.class); + Assert.assertEquals("this is an injection test", object.val); + Assert.assertEquals(object.val.length(), object.valLen); + Assert.assertNull(object.injected); + } + + private static class ClassOfMultipleJsonCreators + { + private final String val; + private final int valLen; + @Nullable + private final InjectedParameter injected; + + @JsonCreator + public ClassOfMultipleJsonCreators( + @JsonProperty("val") String val, + @JsonProperty("valLen") int valLen, + @JacksonInject @Nullable InjectedParameter injected + ) + { + this.val = val; + this.valLen = valLen; + this.injected = injected; + } + + @JsonCreator + public static ClassOfMultipleJsonCreators create(String val) + { + return new ClassOfMultipleJsonCreators(val, val.length(), null); + } + + @JsonProperty + public String getVal() + { + return val; + } + + @JsonProperty + public int getValLen() + { + return valLen; + } + } + } + + private static class InjectedParameter + { + @JsonProperty + private String val; + } + + private static Injector makeInjectorWithProperties(final Properties props) + { + return Guice.createInjector( + ImmutableList.of( + new DruidGuiceExtensions(), + (Module) binder -> { + binder.bind(Validator.class).toInstance(Validation.buildDefaultValidatorFactory().getValidator()); + binder.bind(JsonConfigurator.class).in(LazySingleton.class); + binder.bind(Properties.class).toInstance(props); + JsonConfigProvider.bind(binder, "druid.injected", InjectedParameter.class); + } + ) + ); + } + + private static ObjectMapper makeObjectMapper(Injector injector) + { + final ObjectMapper mapper = new ObjectMapper(); + DruidSecondaryModule.setupJackson(injector, mapper); + return mapper; + } +} diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/common/TestUtils.java b/indexing-service/src/test/java/org/apache/druid/indexing/common/TestUtils.java index c36a072b34e..fe8cadf4ec3 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/common/TestUtils.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/common/TestUtils.java @@ -30,6 +30,7 @@ import org.apache.druid.client.indexing.IndexingServiceClient; import org.apache.druid.client.indexing.NoopIndexingServiceClient; import org.apache.druid.data.input.impl.NoopInputFormat; import org.apache.druid.data.input.impl.NoopInputSource; +import org.apache.druid.guice.DruidSecondaryModule; import org.apache.druid.guice.FirehoseModule; import org.apache.druid.indexing.common.stats.DropwizardRowIngestionMetersFactory; import org.apache.druid.indexing.common.task.IndexTaskClientFactory; @@ -43,6 +44,7 @@ import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.expression.LookupEnabledTestExprMacroTable; import org.apache.druid.segment.IndexIO; import org.apache.druid.segment.IndexMergerV9; +import org.apache.druid.segment.TestHelper; import org.apache.druid.segment.incremental.RowIngestionMetersFactory; import org.apache.druid.segment.loading.LocalDataSegmentPuller; import org.apache.druid.segment.loading.LocalLoadSpec; @@ -115,6 +117,7 @@ public class TestUtils } } ); + DruidSecondaryModule.setupAnnotationIntrospector(jsonMapper, TestHelper.makeAnnotationIntrospector()); List firehoseModules = new FirehoseModule().getJacksonModules(); firehoseModules.forEach(jsonMapper::registerModule); diff --git a/processing/src/test/java/org/apache/druid/segment/TestHelper.java b/processing/src/test/java/org/apache/druid/segment/TestHelper.java index a6eb8b03ce1..835d8b098d9 100644 --- a/processing/src/test/java/org/apache/druid/segment/TestHelper.java +++ b/processing/src/test/java/org/apache/druid/segment/TestHelper.java @@ -19,12 +19,16 @@ package org.apache.druid.segment; +import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import org.apache.druid.data.input.MapBasedRow; import org.apache.druid.data.input.Row; +import org.apache.druid.guice.DruidSecondaryModule; +import org.apache.druid.guice.GuiceAnnotationIntrospector; import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.java.util.common.guava.Sequence; @@ -71,9 +75,26 @@ public class TestHelper return new IndexIO(JSON_MAPPER, columnConfig); } + public static AnnotationIntrospector makeAnnotationIntrospector() + { + // Prepare annotationIntrospector with similar logic, except skip Guice loading + // because most tests don't use Guice injection. + return new GuiceAnnotationIntrospector() + { + @Override + public Object findInjectableValueId(AnnotatedMember m) + { + return null; + } + }; + } + public static ObjectMapper makeJsonMapper() { final ObjectMapper mapper = new DefaultObjectMapper(); + AnnotationIntrospector introspector = makeAnnotationIntrospector(); + DruidSecondaryModule.setupAnnotationIntrospector(mapper, introspector); + mapper.setInjectableValues( new InjectableValues.Std() .addValue(ExprMacroTable.class.getName(), TestExprMacroTable.INSTANCE) @@ -86,6 +107,7 @@ public class TestHelper public static ObjectMapper makeSmileMapper() { final ObjectMapper mapper = new DefaultObjectMapper(); + DruidSecondaryModule.setupAnnotationIntrospector(mapper, makeAnnotationIntrospector()); mapper.setInjectableValues( new InjectableValues.Std() .addValue(ExprMacroTable.class.getName(), TestExprMacroTable.INSTANCE)