Fix Smile encoding for HTTP response (#10980)
* fix Smile encoding bug
Signed-off-by: frank chen <frank.chen021@outlook.com>
* Add unit tests
* Add IT for smile encoding
* Fix cases
* Update javadoc
Co-authored-by: Jihoon Son <jihoonson@apache.org>
* resolve comments
Co-authored-by: Jihoon Son <jihoonson@apache.org>
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/clients/AbstractQueryResourceTestClient.java b/integration-tests/src/main/java/org/apache/druid/testing/clients/AbstractQueryResourceTestClient.java
index 15851c5..ce2703c 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/clients/AbstractQueryResourceTestClient.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/clients/AbstractQueryResourceTestClient.java
@@ -21,38 +21,115 @@
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.jaxrs.smile.SmileMediaTypes;
import com.google.inject.Inject;
+import org.apache.druid.guice.annotations.Smile;
+import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.http.client.HttpClient;
import org.apache.druid.java.util.http.client.Request;
+import org.apache.druid.java.util.http.client.response.BytesFullResponseHandler;
+import org.apache.druid.java.util.http.client.response.BytesFullResponseHolder;
import org.apache.druid.java.util.http.client.response.StatusResponseHandler;
import org.apache.druid.java.util.http.client.response.StatusResponseHolder;
-import org.apache.druid.testing.IntegrationTestingConfig;
import org.apache.druid.testing.guice.TestClient;
+import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
+import javax.annotation.Nullable;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
public abstract class AbstractQueryResourceTestClient<QueryType>
{
- private final ObjectMapper jsonMapper;
- private final HttpClient httpClient;
- final String routerUrl;
+ final String contentTypeHeader;
+ /**
+ * a 'null' means the Content-Type in response defaults to Content-Type of request
+ */
+ final String acceptHeader;
+
+ final ObjectMapper jsonMapper;
+ final ObjectMapper smileMapper;
+ final HttpClient httpClient;
+ final String routerUrl;
+ final Map<String, EncoderDecoder> encoderDecoderMap;
+
+ /**
+ * A encoder/decoder that encodes/decodes requests/responses based on Content-Type.
+ */
+ interface EncoderDecoder
+ {
+ byte[] encode(Object content) throws IOException;
+
+ List<Map<String, Object>> decode(byte[] content) throws IOException;
+ }
+
+ static class ObjectMapperEncoderDecoder implements EncoderDecoder
+ {
+ private final ObjectMapper om;
+
+ ObjectMapperEncoderDecoder(ObjectMapper om)
+ {
+ this.om = om;
+ }
+
+ @Override
+ public byte[] encode(Object content) throws IOException
+ {
+ return om.writeValueAsBytes(content);
+ }
+
+ @Override
+ public List<Map<String, Object>> decode(byte[] content) throws IOException
+ {
+ return om.readValue(content, new TypeReference<List<Map<String, Object>>>()
+ {
+ });
+ }
+ }
+
+ /**
+ * @param contentTypeHeader Content-Type header of HTTP request
+ * @param acceptHeader Accept header of HTTP request. If it's null, Content-Type in response defaults to Content-Type in request
+ */
@Inject
AbstractQueryResourceTestClient(
ObjectMapper jsonMapper,
+ @Smile ObjectMapper smileMapper,
@TestClient HttpClient httpClient,
- IntegrationTestingConfig config
+ String routerUrl,
+ String contentTypeHeader,
+ @Nullable String acceptHeader
)
{
this.jsonMapper = jsonMapper;
+ this.smileMapper = smileMapper;
this.httpClient = httpClient;
- this.routerUrl = config.getRouterUrl();
+ this.routerUrl = routerUrl;
+
+ this.encoderDecoderMap = new HashMap<>();
+ this.encoderDecoderMap.put(MediaType.APPLICATION_JSON, new ObjectMapperEncoderDecoder(jsonMapper));
+ this.encoderDecoderMap.put(SmileMediaTypes.APPLICATION_JACKSON_SMILE, new ObjectMapperEncoderDecoder(smileMapper));
+
+ if (!this.encoderDecoderMap.containsKey(contentTypeHeader)) {
+ throw new IAE("Invalid Content-Type[%s]", contentTypeHeader);
+ }
+ this.contentTypeHeader = contentTypeHeader;
+
+ if (acceptHeader != null) {
+ if (!this.encoderDecoderMap.containsKey(acceptHeader)) {
+ throw new IAE("Invalid Accept[%s]", acceptHeader);
+ }
+ }
+ this.acceptHeader = acceptHeader;
}
public abstract String getBrokerURL();
@@ -60,12 +137,18 @@
public List<Map<String, Object>> query(String url, QueryType query)
{
try {
- StatusResponseHolder response = httpClient.go(
- new Request(HttpMethod.POST, new URL(url)).setContent(
- "application/json",
- jsonMapper.writeValueAsBytes(query)
- ),
- StatusResponseHandler.getInstance()
+ String expectedResponseType = this.contentTypeHeader;
+
+ Request request = new Request(HttpMethod.POST, new URL(url));
+ request.setContent(this.contentTypeHeader, encoderDecoderMap.get(this.contentTypeHeader).encode(query));
+ if (this.acceptHeader != null) {
+ expectedResponseType = this.acceptHeader;
+ request.addHeader(HttpHeaders.Names.ACCEPT, this.acceptHeader);
+ }
+
+ BytesFullResponseHolder response = httpClient.go(
+ request,
+ new BytesFullResponseHandler()
).get();
if (!response.getStatus().equals(HttpResponseStatus.OK)) {
@@ -73,15 +156,20 @@
"Error while querying[%s] status[%s] content[%s]",
getBrokerURL(),
response.getStatus(),
- response.getContent()
+ new String(response.getContent(), StandardCharsets.UTF_8)
);
}
- return jsonMapper.readValue(
- response.getContent(), new TypeReference<List<Map<String, Object>>>()
- {
- }
- );
+ String responseType = response.getResponse().headers().get(HttpHeaders.Names.CONTENT_TYPE);
+ if (!expectedResponseType.equals(responseType)) {
+ throw new ISE(
+ "Content-Type[%s] in HTTP response does not match the expected[%s]",
+ responseType,
+ expectedResponseType
+ );
+ }
+
+ return this.encoderDecoderMap.get(responseType).decode(response.getContent());
}
catch (Exception e) {
throw new RuntimeException(e);
@@ -91,11 +179,10 @@
public Future<StatusResponseHolder> queryAsync(String url, QueryType query)
{
try {
+ Request request = new Request(HttpMethod.POST, new URL(url));
+ request.setContent(MediaType.APPLICATION_JSON, encoderDecoderMap.get(MediaType.APPLICATION_JSON).encode(query));
return httpClient.go(
- new Request(HttpMethod.POST, new URL(url)).setContent(
- "application/json",
- jsonMapper.writeValueAsBytes(query)
- ),
+ request,
StatusResponseHandler.getInstance()
);
}
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/clients/QueryResourceTestClient.java b/integration-tests/src/main/java/org/apache/druid/testing/clients/QueryResourceTestClient.java
index 1397fe1..33820fe 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/clients/QueryResourceTestClient.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/clients/QueryResourceTestClient.java
@@ -22,23 +22,40 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Inject;
+import org.apache.druid.guice.annotations.Smile;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.http.client.HttpClient;
import org.apache.druid.query.Query;
import org.apache.druid.testing.IntegrationTestingConfig;
import org.apache.druid.testing.guice.TestClient;
+import javax.annotation.Nullable;
+import javax.ws.rs.core.MediaType;
+
public class QueryResourceTestClient extends AbstractQueryResourceTestClient<Query>
{
@Inject
QueryResourceTestClient(
ObjectMapper jsonMapper,
+ @Smile ObjectMapper smileMapper,
@TestClient HttpClient httpClient,
IntegrationTestingConfig config
)
{
- super(jsonMapper, httpClient, config);
+ this(jsonMapper, smileMapper, httpClient, config.getRouterUrl(), MediaType.APPLICATION_JSON, null);
+ }
+
+ private QueryResourceTestClient(
+ ObjectMapper jsonMapper,
+ @Smile ObjectMapper smileMapper,
+ @TestClient HttpClient httpClient,
+ String routerUrl,
+ String contentType,
+ String accept
+ )
+ {
+ super(jsonMapper, smileMapper, httpClient, routerUrl, contentType, accept);
}
@Override
@@ -50,4 +67,22 @@
);
}
+ /**
+ * clone a new instance of current object with given encoding.
+ * Note: For {@link AbstractQueryResourceTestClient#queryAsync(String, Object)} operation, contentType could only be application/json
+ *
+ * @param contentType Content-Type header of request. Cannot be NULL. Both application/json and application/x-jackson-smile are allowed
+ * @param accept Accept header of request. Both application/json and application/x-jackson-smile are allowed
+ */
+ public QueryResourceTestClient withEncoding(String contentType, @Nullable String accept)
+ {
+ return new QueryResourceTestClient(
+ this.jsonMapper,
+ this.smileMapper,
+ this.httpClient,
+ this.routerUrl,
+ contentType,
+ accept
+ );
+ }
}
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/clients/SqlResourceTestClient.java b/integration-tests/src/main/java/org/apache/druid/testing/clients/SqlResourceTestClient.java
index d8f85c6..b429318 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/clients/SqlResourceTestClient.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/clients/SqlResourceTestClient.java
@@ -27,6 +27,8 @@
import org.apache.druid.testing.IntegrationTestingConfig;
import org.apache.druid.testing.guice.TestClient;
+import javax.ws.rs.core.MediaType;
+
public class SqlResourceTestClient extends AbstractQueryResourceTestClient<SqlQuery>
{
@@ -37,7 +39,9 @@
IntegrationTestingConfig config
)
{
- super(jsonMapper, httpClient, config);
+ // currently smile encoding is not supported on SQL endpoint
+ // so no need to pass smile ObjectMapper
+ super(jsonMapper, null, httpClient, config.getRouterUrl(), MediaType.APPLICATION_JSON, null);
}
@Override
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/utils/AbstractTestQueryHelper.java b/integration-tests/src/main/java/org/apache/druid/testing/utils/AbstractTestQueryHelper.java
index ebaed72..bd6866a 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/utils/AbstractTestQueryHelper.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/utils/AbstractTestQueryHelper.java
@@ -43,8 +43,8 @@
public static final Logger LOG = new Logger(TestQueryHelper.class);
- private final AbstractQueryResourceTestClient queryClient;
- private final ObjectMapper jsonMapper;
+ protected final AbstractQueryResourceTestClient queryClient;
+ protected final ObjectMapper jsonMapper;
protected final String broker;
protected final String brokerTLS;
protected final String router;
@@ -57,12 +57,31 @@
IntegrationTestingConfig config
)
{
+ this(
+ jsonMapper,
+ queryClient,
+ config.getBrokerUrl(),
+ config.getBrokerTLSUrl(),
+ config.getRouterUrl(),
+ config.getRouterTLSUrl()
+ );
+ }
+
+ AbstractTestQueryHelper(
+ ObjectMapper jsonMapper,
+ AbstractQueryResourceTestClient queryClient,
+ String broker,
+ String brokerTLS,
+ String router,
+ String routerTLS
+ )
+ {
this.jsonMapper = jsonMapper;
this.queryClient = queryClient;
- this.broker = config.getBrokerUrl();
- this.brokerTLS = config.getBrokerTLSUrl();
- this.router = config.getRouterUrl();
- this.routerTLS = config.getRouterTLSUrl();
+ this.broker = broker;
+ this.brokerTLS = brokerTLS;
+ this.router = router;
+ this.routerTLS = routerTLS;
}
public abstract String getQueryURL(String schemeAndHost);
diff --git a/integration-tests/src/main/java/org/apache/druid/testing/utils/TestQueryHelper.java b/integration-tests/src/main/java/org/apache/druid/testing/utils/TestQueryHelper.java
index a79332a..fb48b73 100644
--- a/integration-tests/src/main/java/org/apache/druid/testing/utils/TestQueryHelper.java
+++ b/integration-tests/src/main/java/org/apache/druid/testing/utils/TestQueryHelper.java
@@ -25,6 +25,9 @@
import org.apache.druid.testing.IntegrationTestingConfig;
import org.apache.druid.testing.clients.QueryResourceTestClient;
+import javax.annotation.Nullable;
+import javax.validation.constraints.NotNull;
+
public class TestQueryHelper extends AbstractTestQueryHelper<QueryWithResults>
{
@@ -38,10 +41,46 @@
super(jsonMapper, queryClient, config);
}
+ private TestQueryHelper(
+ ObjectMapper jsonMapper,
+ QueryResourceTestClient queryResourceTestClient,
+ String broker,
+ String brokerTLS,
+ String router,
+ String routerTLS
+ )
+ {
+ super(
+ jsonMapper,
+ queryResourceTestClient,
+ broker,
+ brokerTLS,
+ router,
+ routerTLS
+ );
+ }
+
@Override
public String getQueryURL(String schemeAndHost)
{
return StringUtils.format("%s/druid/v2?pretty", schemeAndHost);
}
+ /**
+ * clone a new instance of current object with given encoding
+ *
+ * @param contentType Content-Type header of request. Cannot be NULL. Both application/json and application/x-jackson-smile are allowed
+ * @param accept Accept header of request. Both application/json and application/x-jackson-smile are allowed
+ */
+ public TestQueryHelper withEncoding(@NotNull String contentType, @Nullable String accept)
+ {
+ return new TestQueryHelper(
+ this.jsonMapper,
+ ((QueryResourceTestClient) this.queryClient).withEncoding(contentType, accept),
+ this.broker,
+ this.brokerTLS,
+ this.router,
+ this.routerTLS
+ );
+ }
}
diff --git a/integration-tests/src/test/java/org/apache/druid/tests/query/ITWikipediaQueryTest.java b/integration-tests/src/test/java/org/apache/druid/tests/query/ITWikipediaQueryTest.java
index 9fc5209..ee53ab5 100644
--- a/integration-tests/src/test/java/org/apache/druid/tests/query/ITWikipediaQueryTest.java
+++ b/integration-tests/src/test/java/org/apache/druid/tests/query/ITWikipediaQueryTest.java
@@ -19,6 +19,7 @@
package org.apache.druid.tests.query;
+import com.fasterxml.jackson.jaxrs.smile.SmileMediaTypes;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import org.apache.druid.java.util.common.logger.Logger;
@@ -36,9 +37,11 @@
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
+import javax.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -79,9 +82,32 @@
}
}
- @Test
- public void testWikipediaQueriesFromFile() throws Exception
+ /**
+ * A combination of request Content-Type and Accept HTTP header
+ * The first is Content-Type which can not be null while the 2nd is Accept which could be null
+ * <p>
+ * When Accept is null, its value defaults to value of Content-Type
+ */
+ @DataProvider
+ public static Object[][] encodingCombination()
{
+ return new Object[][]{
+ {MediaType.APPLICATION_JSON, null},
+ {MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON},
+ {MediaType.APPLICATION_JSON, SmileMediaTypes.APPLICATION_JACKSON_SMILE},
+ {SmileMediaTypes.APPLICATION_JACKSON_SMILE, null},
+ {SmileMediaTypes.APPLICATION_JACKSON_SMILE, MediaType.APPLICATION_JSON},
+ {SmileMediaTypes.APPLICATION_JACKSON_SMILE, SmileMediaTypes.APPLICATION_JACKSON_SMILE},
+ };
+ }
+
+ @Test(dataProvider = "encodingCombination")
+ public void testWikipediaQueriesFromFile(String contentType, String accept)
+ throws Exception
+ {
+ // run tests on a new query helper
+ TestQueryHelper queryHelper = this.queryHelper.withEncoding(contentType, accept);
+
queryHelper.testQueriesFromFile(WIKIPEDIA_QUERIES_RESOURCE);
}
diff --git a/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java b/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java
index 9ef86af..b4ebc8c 100644
--- a/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java
+++ b/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java
@@ -91,11 +91,10 @@
@Context final HttpServletRequest req
) throws IOException
{
- final ResourceIOReaderWriter ioReaderWriter =
- createResourceIOReaderWriter(req.getContentType(), pretty != null);
+ final ResourceIOReaderWriter ioReaderWriter = createResourceIOReaderWriter(req, pretty != null);
try {
- Query<?> query = ioReaderWriter.getInputMapper().readValue(in, Query.class);
- return ioReaderWriter.ok(
+ Query<?> query = ioReaderWriter.getRequestMapper().readValue(in, Query.class);
+ return ioReaderWriter.getResponseWriter().ok(
ServerViewUtil.getTargetLocations(
brokerServerView,
query.getDataSource(),
@@ -105,7 +104,7 @@
);
}
catch (Exception e) {
- return ioReaderWriter.gotError(e);
+ return ioReaderWriter.getResponseWriter().gotError(e);
}
}
}
diff --git a/server/src/main/java/org/apache/druid/server/QueryResource.java b/server/src/main/java/org/apache/druid/server/QueryResource.java
index 6394395..b2f7f21 100644
--- a/server/src/main/java/org/apache/druid/server/QueryResource.java
+++ b/server/src/main/java/org/apache/druid/server/QueryResource.java
@@ -183,13 +183,7 @@
final QueryLifecycle queryLifecycle = queryLifecycleFactory.factorize();
Query<?> query = null;
- String acceptHeader = req.getHeader("Accept");
- if (Strings.isNullOrEmpty(acceptHeader)) {
- //default to content-type
- acceptHeader = req.getContentType();
- }
-
- final ResourceIOReaderWriter ioReaderWriter = createResourceIOReaderWriter(acceptHeader, pretty != null);
+ final ResourceIOReaderWriter ioReaderWriter = createResourceIOReaderWriter(req, pretty != null);
final String currThreadName = Thread.currentThread().getName();
try {
@@ -235,7 +229,7 @@
QueryContexts.isSerializeDateTimeAsLong(query, false)
|| (!shouldFinalize && QueryContexts.isSerializeDateTimeAsLongInner(query, false));
- final ObjectWriter jsonWriter = ioReaderWriter.newOutputWriter(
+ final ObjectWriter jsonWriter = ioReaderWriter.getResponseWriter().newOutputWriter(
queryLifecycle.getToolChest(),
queryLifecycle.getQuery(),
serializeDateTimeAsLong
@@ -276,7 +270,7 @@
}
}
},
- ioReaderWriter.getContentType()
+ ioReaderWriter.getResponseWriter().getResponseType()
)
.header("X-Druid-Query-Id", queryId);
@@ -337,27 +331,27 @@
catch (QueryInterruptedException e) {
interruptedQueryCount.incrementAndGet();
queryLifecycle.emitLogsAndMetrics(e, req.getRemoteAddr(), -1);
- return ioReaderWriter.gotError(e);
+ return ioReaderWriter.getResponseWriter().gotError(e);
}
catch (QueryTimeoutException timeout) {
timedOutQueryCount.incrementAndGet();
queryLifecycle.emitLogsAndMetrics(timeout, req.getRemoteAddr(), -1);
- return ioReaderWriter.gotTimeout(timeout);
+ return ioReaderWriter.getResponseWriter().gotTimeout(timeout);
}
catch (QueryCapacityExceededException cap) {
failedQueryCount.incrementAndGet();
queryLifecycle.emitLogsAndMetrics(cap, req.getRemoteAddr(), -1);
- return ioReaderWriter.gotLimited(cap);
+ return ioReaderWriter.getResponseWriter().gotLimited(cap);
}
catch (QueryUnsupportedException unsupported) {
failedQueryCount.incrementAndGet();
queryLifecycle.emitLogsAndMetrics(unsupported, req.getRemoteAddr(), -1);
- return ioReaderWriter.gotUnsupported(unsupported);
+ return ioReaderWriter.getResponseWriter().gotUnsupported(unsupported);
}
catch (BadJsonQueryException | ResourceLimitExceededException e) {
interruptedQueryCount.incrementAndGet();
queryLifecycle.emitLogsAndMetrics(e, req.getRemoteAddr(), -1);
- return ioReaderWriter.gotBadQuery(e);
+ return ioReaderWriter.getResponseWriter().gotBadQuery(e);
}
catch (ForbiddenException e) {
// don't do anything for an authorization failure, ForbiddenExceptionMapper will catch this later and
@@ -374,7 +368,7 @@
.addData("peer", req.getRemoteAddr())
.emit();
- return ioReaderWriter.gotError(e);
+ return ioReaderWriter.getResponseWriter().gotError(e);
}
finally {
Thread.currentThread().setName(currThreadName);
@@ -389,7 +383,7 @@
{
Query baseQuery;
try {
- baseQuery = ioReaderWriter.getInputMapper().readValue(in, Query.class);
+ baseQuery = ioReaderWriter.getRequestMapper().readValue(in, Query.class);
}
catch (JsonParseException e) {
throw new BadJsonQueryException(e);
@@ -415,47 +409,71 @@
return mapper.copy().registerModule(new SimpleModule().addSerializer(DateTime.class, new DateTimeSerializer()));
}
- protected ResourceIOReaderWriter createResourceIOReaderWriter(String requestType, boolean pretty)
+ protected ResourceIOReaderWriter createResourceIOReaderWriter(HttpServletRequest req, boolean pretty)
{
- boolean isSmile = SmileMediaTypes.APPLICATION_JACKSON_SMILE.equals(requestType) ||
- APPLICATION_SMILE.equals(requestType);
- String contentType = isSmile ? SmileMediaTypes.APPLICATION_JACKSON_SMILE : MediaType.APPLICATION_JSON;
+ String requestType = req.getContentType();
+ String acceptHeader = req.getHeader("Accept");
+
+ // response type defaults to Content-Type if 'Accept' header not provided
+ String responseType = Strings.isNullOrEmpty(acceptHeader) ? requestType : acceptHeader;
+
+ boolean isRequestSmile = SmileMediaTypes.APPLICATION_JACKSON_SMILE.equals(requestType) || APPLICATION_SMILE.equals(requestType);
+ boolean isResponseSmile = SmileMediaTypes.APPLICATION_JACKSON_SMILE.equals(responseType) || APPLICATION_SMILE.equals(responseType);
+
return new ResourceIOReaderWriter(
- contentType,
- isSmile ? smileMapper : jsonMapper,
- isSmile ? serializeDateTimeAsLongSmileMapper : serializeDateTimeAsLongJsonMapper,
- pretty
- );
+ isRequestSmile ? smileMapper : jsonMapper,
+ new ResourceIOWriter(isResponseSmile ? SmileMediaTypes.APPLICATION_JACKSON_SMILE : MediaType.APPLICATION_JSON,
+ isResponseSmile ? smileMapper : jsonMapper,
+ isResponseSmile ? serializeDateTimeAsLongSmileMapper : serializeDateTimeAsLongJsonMapper,
+ pretty
+ ));
}
protected static class ResourceIOReaderWriter
{
- private final String contentType;
+ private final ObjectMapper requestMapper;
+ private final ResourceIOWriter writer;
+
+ public ResourceIOReaderWriter(ObjectMapper requestMapper, ResourceIOWriter writer)
+ {
+ this.requestMapper = requestMapper;
+ this.writer = writer;
+ }
+
+ public ObjectMapper getRequestMapper()
+ {
+ return requestMapper;
+ }
+
+ public ResourceIOWriter getResponseWriter()
+ {
+ return writer;
+ }
+ }
+
+ protected static class ResourceIOWriter
+ {
+ private final String responseType;
private final ObjectMapper inputMapper;
private final ObjectMapper serializeDateTimeAsLongInputMapper;
private final boolean isPretty;
- ResourceIOReaderWriter(
- String contentType,
+ ResourceIOWriter(
+ String responseType,
ObjectMapper inputMapper,
ObjectMapper serializeDateTimeAsLongInputMapper,
boolean isPretty
)
{
- this.contentType = contentType;
+ this.responseType = responseType;
this.inputMapper = inputMapper;
this.serializeDateTimeAsLongInputMapper = serializeDateTimeAsLongInputMapper;
this.isPretty = isPretty;
}
- String getContentType()
+ String getResponseType()
{
- return contentType;
- }
-
- ObjectMapper getInputMapper()
- {
- return inputMapper;
+ return responseType;
}
ObjectWriter newOutputWriter(
@@ -476,7 +494,7 @@
Response ok(Object object) throws IOException
{
- return Response.ok(newOutputWriter(null, null, false).writeValueAsString(object), contentType).build();
+ return Response.ok(newOutputWriter(null, null, false).writeValueAsString(object), responseType).build();
}
Response gotError(Exception e) throws IOException
@@ -510,7 +528,7 @@
Response buildNonOkResponse(int status, Exception e) throws JsonProcessingException
{
return Response.status(status)
- .type(contentType)
+ .type(responseType)
.entity(newOutputWriter(null, null, false).writeValueAsBytes(e))
.build();
}
diff --git a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
index ce3c032..026af18 100644
--- a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
+++ b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
@@ -28,6 +28,10 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import org.apache.druid.guice.GuiceInjectors;
+import org.apache.druid.guice.annotations.Smile;
import org.apache.druid.jackson.DefaultObjectMapper;
import org.apache.druid.java.util.common.concurrent.Execs;
import org.apache.druid.java.util.common.guava.LazySequence;
@@ -96,7 +100,6 @@
public class QueryResourceTest
{
private static final QueryToolChestWarehouse WAREHOUSE = new MapQueryToolChestWarehouse(ImmutableMap.of());
- private static final ObjectMapper JSON_MAPPER = new DefaultObjectMapper();
private static final AuthenticationResult AUTHENTICATION_RESULT =
new AuthenticationResult("druid", "druid", null, null);
@@ -178,6 +181,8 @@
false
);
+ private ObjectMapper jsonMapper;
+ private ObjectMapper smileMapper;
private QueryResource queryResource;
private QueryScheduler queryScheduler;
private TestRequestLogger testRequestLogger;
@@ -191,6 +196,10 @@
@Before
public void setup()
{
+ Injector injector = GuiceInjectors.makeStartupInjector();
+ jsonMapper = injector.getInstance(ObjectMapper.class);
+ smileMapper = injector.getInstance(Key.get(ObjectMapper.class, Smile.class));
+
EasyMock.expect(testServletRequest.getContentType()).andReturn(MediaType.APPLICATION_JSON).anyTimes();
EasyMock.expect(testServletRequest.getHeader("Accept")).andReturn(MediaType.APPLICATION_JSON).anyTimes();
EasyMock.expect(testServletRequest.getHeader(QueryResource.HEADER_IF_NONE_MATCH)).andReturn(null).anyTimes();
@@ -213,8 +222,8 @@
AuthTestUtils.TEST_AUTHORIZER_MAPPER,
Suppliers.ofInstance(new DefaultQueryConfig(ImmutableMap.of()))
),
- JSON_MAPPER,
- JSON_MAPPER,
+ jsonMapper,
+ smileMapper,
queryScheduler,
new AuthConfig(),
null,
@@ -259,8 +268,8 @@
AuthTestUtils.TEST_AUTHORIZER_MAPPER,
Suppliers.ofInstance(overrideConfig)
),
- JSON_MAPPER,
- JSON_MAPPER,
+ jsonMapper,
+ smileMapper,
queryScheduler,
new AuthConfig(),
null,
@@ -279,7 +288,7 @@
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
((StreamingOutput) response.getEntity()).write(baos);
- final List<Result<TimeBoundaryResultValue>> responses = JSON_MAPPER.readValue(
+ final List<Result<TimeBoundaryResultValue>> responses = jsonMapper.readValue(
baos.toByteArray(),
new TypeReference<List<Result<TimeBoundaryResultValue>>>() {}
);
@@ -311,8 +320,8 @@
AuthTestUtils.TEST_AUTHORIZER_MAPPER,
Suppliers.ofInstance(overrideConfig)
),
- JSON_MAPPER,
- JSON_MAPPER,
+ jsonMapper,
+ smileMapper,
queryScheduler,
new AuthConfig(),
null,
@@ -332,7 +341,7 @@
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
((StreamingOutput) response.getEntity()).write(baos);
- final List<Result<TimeBoundaryResultValue>> responses = JSON_MAPPER.readValue(
+ final List<Result<TimeBoundaryResultValue>> responses = jsonMapper.readValue(
baos.toByteArray(),
new TypeReference<List<Result<TimeBoundaryResultValue>>>() {}
);
@@ -367,7 +376,7 @@
).toString();
Assert.assertEquals(
expectedException,
- JSON_MAPPER.readValue((byte[]) response.getEntity(), QueryInterruptedException.class).toString()
+ jsonMapper.readValue((byte[]) response.getEntity(), QueryInterruptedException.class).toString()
);
}
@@ -457,7 +466,7 @@
}
@Test
- public void testGoodQueryWithSmileAcceptHeader() throws IOException
+ public void testGoodQueryWithJsonRequestAndSmileAcceptHeader() throws IOException
{
//Doing a replay of testServletRequest for teardown to succeed.
//We dont use testServletRequest in this testcase
@@ -466,6 +475,8 @@
//Creating our own Smile Servlet request, as to not disturb the remaining tests.
// else refactoring required for this class. i know this kinda makes the class somewhat Dirty.
final HttpServletRequest smileRequest = EasyMock.createMock(HttpServletRequest.class);
+
+ // Set Content-Type to JSON
EasyMock.expect(smileRequest.getContentType()).andReturn(MediaType.APPLICATION_JSON).anyTimes();
EasyMock.expect(smileRequest.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED))
@@ -479,6 +490,7 @@
smileRequest.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
+ // Set Accept to Smile
EasyMock.expect(smileRequest.getHeader("Accept")).andReturn(SmileMediaTypes.APPLICATION_JACKSON_SMILE).anyTimes();
EasyMock.expect(smileRequest.getHeader(QueryResource.HEADER_IF_NONE_MATCH)).andReturn(null).anyTimes();
EasyMock.expect(smileRequest.getRemoteAddr()).andReturn("localhost").anyTimes();
@@ -490,6 +502,98 @@
smileRequest
);
Assert.assertEquals(HttpStatus.SC_OK, response.getStatus());
+
+ // Content-Type in response should be Smile
+ Assert.assertEquals(SmileMediaTypes.APPLICATION_JACKSON_SMILE, (response.getMetadata().get("Content-Type").get(0)).toString());
+ Assert.assertNotNull(response);
+ EasyMock.verify(smileRequest);
+ }
+
+ @Test
+ public void testGoodQueryWithSmileRequestAndSmileAcceptHeader() throws IOException
+ {
+ //Doing a replay of testServletRequest for teardown to succeed.
+ //We dont use testServletRequest in this testcase
+ EasyMock.replay(testServletRequest);
+
+ //Creating our own Smile Servlet request, as to not disturb the remaining tests.
+ // else refactoring required for this class. i know this kinda makes the class somewhat Dirty.
+ final HttpServletRequest smileRequest = EasyMock.createMock(HttpServletRequest.class);
+
+ // Set Content-Type to Smile
+ EasyMock.expect(smileRequest.getContentType()).andReturn(SmileMediaTypes.APPLICATION_JACKSON_SMILE).anyTimes();
+
+ EasyMock.expect(smileRequest.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED))
+ .andReturn(null)
+ .anyTimes();
+ EasyMock.expect(smileRequest.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).anyTimes();
+
+ EasyMock.expect(smileRequest.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT))
+ .andReturn(AUTHENTICATION_RESULT)
+ .anyTimes();
+
+ smileRequest.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
+
+ // Set Accept to Smile
+ EasyMock.expect(smileRequest.getHeader("Accept")).andReturn(SmileMediaTypes.APPLICATION_JACKSON_SMILE).anyTimes();
+ EasyMock.expect(smileRequest.getHeader(QueryResource.HEADER_IF_NONE_MATCH)).andReturn(null).anyTimes();
+ EasyMock.expect(smileRequest.getRemoteAddr()).andReturn("localhost").anyTimes();
+
+ EasyMock.replay(smileRequest);
+ Response response = queryResource.doPost(
+ // Write input in Smile encoding
+ new ByteArrayInputStream(smileMapper.writeValueAsBytes(jsonMapper.readTree(SIMPLE_TIMESERIES_QUERY))),
+ null /*pretty*/,
+ smileRequest
+ );
+ Assert.assertEquals(HttpStatus.SC_OK, response.getStatus());
+
+ // Content-Type in response should be Smile
+ Assert.assertEquals(SmileMediaTypes.APPLICATION_JACKSON_SMILE, (response.getMetadata().get("Content-Type").get(0)).toString());
+ Assert.assertNotNull(response);
+ EasyMock.verify(smileRequest);
+ }
+
+ @Test
+ public void testGoodQueryWithSmileRequestNoSmileAcceptHeader() throws IOException
+ {
+ //Doing a replay of testServletRequest for teardown to succeed.
+ //We dont use testServletRequest in this testcase
+ EasyMock.replay(testServletRequest);
+
+ //Creating our own Smile Servlet request, as to not disturb the remaining tests.
+ // else refactoring required for this class. i know this kinda makes the class somewhat Dirty.
+ final HttpServletRequest smileRequest = EasyMock.createMock(HttpServletRequest.class);
+
+ // Set Content-Type to Smile
+ EasyMock.expect(smileRequest.getContentType()).andReturn(SmileMediaTypes.APPLICATION_JACKSON_SMILE).anyTimes();
+
+ EasyMock.expect(smileRequest.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED))
+ .andReturn(null)
+ .anyTimes();
+ EasyMock.expect(smileRequest.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).anyTimes();
+
+ EasyMock.expect(smileRequest.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT))
+ .andReturn(AUTHENTICATION_RESULT)
+ .anyTimes();
+
+ smileRequest.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
+
+ // DO NOT set Accept to Smile, Content-Type in response will be default to Content-Type in request
+ EasyMock.expect(smileRequest.getHeader("Accept")).andReturn(null).anyTimes();
+ EasyMock.expect(smileRequest.getHeader(QueryResource.HEADER_IF_NONE_MATCH)).andReturn(null).anyTimes();
+ EasyMock.expect(smileRequest.getRemoteAddr()).andReturn("localhost").anyTimes();
+
+ EasyMock.replay(smileRequest);
+ Response response = queryResource.doPost(
+ // Write input in Smile encoding
+ new ByteArrayInputStream(smileMapper.writeValueAsBytes(jsonMapper.readTree(SIMPLE_TIMESERIES_QUERY))),
+ null /*pretty*/,
+ smileRequest
+ );
+ Assert.assertEquals(HttpStatus.SC_OK, response.getStatus());
+
+ // Content-Type in response will be default to Content-Type in request
Assert.assertEquals(SmileMediaTypes.APPLICATION_JACKSON_SMILE, (response.getMetadata().get("Content-Type").get(0)).toString());
Assert.assertNotNull(response);
EasyMock.verify(smileRequest);
@@ -506,7 +610,7 @@
);
Assert.assertNotNull(response);
Assert.assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
- QueryException e = JSON_MAPPER.readValue((byte[]) response.getEntity(), QueryException.class);
+ QueryException e = jsonMapper.readValue((byte[]) response.getEntity(), QueryException.class);
Assert.assertEquals(BadJsonQueryException.ERROR_CODE, e.getErrorCode());
Assert.assertEquals(BadJsonQueryException.ERROR_CLASS, e.getErrorClass());
}
@@ -525,7 +629,7 @@
);
Assert.assertNotNull(response);
Assert.assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
- QueryException e = JSON_MAPPER.readValue((byte[]) response.getEntity(), QueryException.class);
+ QueryException e = jsonMapper.readValue((byte[]) response.getEntity(), QueryException.class);
Assert.assertEquals(ResourceLimitExceededException.ERROR_CODE, e.getErrorCode());
Assert.assertEquals(ResourceLimitExceededException.class.getName(), e.getErrorClass());
}
@@ -546,7 +650,7 @@
);
Assert.assertNotNull(response);
Assert.assertEquals(QueryUnsupportedException.STATUS_CODE, response.getStatus());
- QueryException ex = JSON_MAPPER.readValue((byte[]) response.getEntity(), QueryException.class);
+ QueryException ex = jsonMapper.readValue((byte[]) response.getEntity(), QueryException.class);
Assert.assertEquals(errorMessage, ex.getMessage());
Assert.assertEquals(QueryUnsupportedException.ERROR_CODE, ex.getErrorCode());
}
@@ -603,8 +707,8 @@
authMapper,
Suppliers.ofInstance(new DefaultQueryConfig(ImmutableMap.of()))
),
- JSON_MAPPER,
- JSON_MAPPER,
+ jsonMapper,
+ smileMapper,
queryScheduler,
new AuthConfig(),
authMapper,
@@ -632,7 +736,7 @@
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
((StreamingOutput) response.getEntity()).write(baos);
- final List<Result<TimeBoundaryResultValue>> responses = JSON_MAPPER.readValue(
+ final List<Result<TimeBoundaryResultValue>> responses = jsonMapper.readValue(
baos.toByteArray(),
new TypeReference<List<Result<TimeBoundaryResultValue>>>() {}
);
@@ -679,8 +783,8 @@
AuthTestUtils.TEST_AUTHORIZER_MAPPER,
Suppliers.ofInstance(new DefaultQueryConfig(ImmutableMap.of()))
),
- JSON_MAPPER,
- JSON_MAPPER,
+ jsonMapper,
+ jsonMapper,
queryScheduler,
new AuthConfig(),
null,
@@ -697,7 +801,7 @@
Assert.assertEquals(QueryTimeoutException.STATUS_CODE, response.getStatus());
QueryTimeoutException ex;
try {
- ex = JSON_MAPPER.readValue((byte[]) response.getEntity(), QueryTimeoutException.class);
+ ex = jsonMapper.readValue((byte[]) response.getEntity(), QueryTimeoutException.class);
}
catch (IOException e) {
throw new RuntimeException(e);
@@ -777,8 +881,8 @@
authMapper,
Suppliers.ofInstance(new DefaultQueryConfig(ImmutableMap.of()))
),
- JSON_MAPPER,
- JSON_MAPPER,
+ jsonMapper,
+ smileMapper,
queryScheduler,
new AuthConfig(),
authMapper,
@@ -901,8 +1005,8 @@
authMapper,
Suppliers.ofInstance(new DefaultQueryConfig(ImmutableMap.of()))
),
- JSON_MAPPER,
- JSON_MAPPER,
+ jsonMapper,
+ smileMapper,
queryScheduler,
new AuthConfig(),
authMapper,
@@ -995,7 +1099,7 @@
Assert.assertEquals(QueryCapacityExceededException.STATUS_CODE, response.getStatus());
QueryCapacityExceededException ex;
try {
- ex = JSON_MAPPER.readValue((byte[]) response.getEntity(), QueryCapacityExceededException.class);
+ ex = jsonMapper.readValue((byte[]) response.getEntity(), QueryCapacityExceededException.class);
}
catch (IOException e) {
throw new RuntimeException(e);
@@ -1036,7 +1140,7 @@
Assert.assertEquals(QueryCapacityExceededException.STATUS_CODE, response.getStatus());
QueryCapacityExceededException ex;
try {
- ex = JSON_MAPPER.readValue((byte[]) response.getEntity(), QueryCapacityExceededException.class);
+ ex = jsonMapper.readValue((byte[]) response.getEntity(), QueryCapacityExceededException.class);
}
catch (IOException e) {
throw new RuntimeException(e);
@@ -1088,7 +1192,7 @@
Assert.assertEquals(QueryCapacityExceededException.STATUS_CODE, response.getStatus());
QueryCapacityExceededException ex;
try {
- ex = JSON_MAPPER.readValue((byte[]) response.getEntity(), QueryCapacityExceededException.class);
+ ex = jsonMapper.readValue((byte[]) response.getEntity(), QueryCapacityExceededException.class);
}
catch (IOException e) {
throw new RuntimeException(e);
@@ -1160,8 +1264,8 @@
AuthTestUtils.TEST_AUTHORIZER_MAPPER,
Suppliers.ofInstance(new DefaultQueryConfig(ImmutableMap.of()))
),
- JSON_MAPPER,
- JSON_MAPPER,
+ jsonMapper,
+ smileMapper,
scheduler,
new AuthConfig(),
null,