diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaType.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaType.java index 2d61120288706..0d2cc5fccb043 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaType.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaType.java @@ -19,6 +19,10 @@ package org.elasticsearch.common.xcontent; +import java.util.Collections; +import java.util.Map; +import java.util.regex.Pattern; + /** * Abstracts a Media Type and a format parameter. * Media types are used as values on Content-Type and Accept headers @@ -46,7 +50,11 @@ public interface MediaType { /** * returns a string representation of a media type. */ - default String typeWithSubtype(){ + default String typeWithSubtype() { return type() + "/" + subtype(); } + + default Map validatedParameters() { + return Collections.emptyMap(); + } } diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeDefinition.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeDefinition.java new file mode 100644 index 0000000000000..f28329eaeb136 --- /dev/null +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeDefinition.java @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.common.xcontent; + +import org.elasticsearch.common.Nullable; + +import java.util.Map; +import java.util.Objects; + +public class MediaTypeDefinition { + + private final String format; + private final String mediaTypeString; + private final Map mediaTypeParameters; + private final MediaType mediaType; + + public MediaTypeDefinition(String mediaTypeString, MediaType mediaType, Map mediaTypeParameters) { + this(null, mediaTypeString, mediaType, mediaTypeParameters); + } + + public MediaTypeDefinition(MediaType mediaType, Map mediaTypeParameters) { + this(mediaType.format(), mediaType.typeWithSubtype(), mediaType, mediaTypeParameters); + } + + private MediaTypeDefinition(String format, String mediaTypeString, MediaType mediaType, Map mediaTypeParameters) { + this.format = format; + this.mediaTypeString = Objects.requireNonNull(mediaTypeString); + this.mediaType = Objects.requireNonNull(mediaType); + this.mediaTypeParameters = Objects.requireNonNull(mediaTypeParameters); + } + + @Nullable + public String format() { + return format; + } + + public String getMediaTypeString() { + return mediaTypeString; + } + + public Map getMediaTypeParameters() { + return mediaTypeParameters; + } + + public MediaType getMediaType() { + return mediaType; + } +} diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeParser.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeParser.java index 62a3f3fd915d0..dec6223b927b1 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeParser.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeParser.java @@ -1,3 +1,4 @@ + /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with @@ -25,27 +26,24 @@ import java.util.regex.Pattern; public class MediaTypeParser { - private final Map formatToMediaType; - private final Map typeWithSubtypeToMediaType; - private final Map> parametersMap; - - public MediaTypeParser(Map formatToMediaType, Map typeWithSubtypeToMediaType, - Map> parametersMap) { - this.formatToMediaType = Map.copyOf(formatToMediaType); - this.typeWithSubtypeToMediaType = Map.copyOf(typeWithSubtypeToMediaType); - this.parametersMap = Map.copyOf(parametersMap); + private final MediaTypeRegistry mediaTypeRegistry; + + public MediaTypeParser(MediaTypeRegistry mediaTypeRegistry) { + this.mediaTypeRegistry = mediaTypeRegistry; } + @SuppressWarnings("unchecked") public T fromMediaType(String mediaType) { ParsedMediaType parsedMediaType = parseMediaType(mediaType); - return parsedMediaType != null ? parsedMediaType.getMediaType() : null; + return parsedMediaType != null ? (T) parsedMediaType.getMediaType() : null; } + @SuppressWarnings("unchecked") public T fromFormat(String format) { if (format == null) { return null; } - return formatToMediaType.get(format.toLowerCase(Locale.ROOT)); + return (T) mediaTypeRegistry.formatToMediaType(format.toLowerCase(Locale.ROOT)); } /** @@ -65,7 +63,7 @@ public ParsedMediaType parseMediaType(String headerValue) { String type = typeSubtype[0]; String subtype = typeSubtype[1]; String typeWithSubtype = type + "/" + subtype; - T xContentType = typeWithSubtypeToMediaType.get(typeWithSubtype); + MediaType xContentType = mediaTypeRegistry.typeWithSubtypeToMediaType(typeWithSubtype); if (xContentType != null) { Map parameters = new HashMap<>(); for (int i = 1; i < split.length; i++) { @@ -90,8 +88,8 @@ public ParsedMediaType parseMediaType(String headerValue) { } private boolean isValidParameter(String typeWithSubtype, String parameterName, String parameterValue) { - if (parametersMap.containsKey(typeWithSubtype)) { - Map parameters = parametersMap.get(typeWithSubtype); + if (mediaTypeRegistry.parametersFor(typeWithSubtype) != null) { + Map parameters = mediaTypeRegistry.parametersFor(typeWithSubtype); if (parameters.containsKey(parameterName)) { Pattern regex = parameters.get(parameterName); return regex.matcher(parameterValue).matches(); @@ -104,19 +102,32 @@ private boolean hasSpaces(String s) { return s.trim().equals(s) == false; } + private static final String COMPATIBLE_WITH_PARAMETER_NAME = "compatible-with"; + + public Byte parseVersion(String mediaType) { + ParsedMediaType parsedMediaType = parseMediaType(mediaType); + if (parsedMediaType != null) { + String version = parsedMediaType + .getParameters() + .get(COMPATIBLE_WITH_PARAMETER_NAME); + return version != null ? Byte.parseByte(version) : null; + } + return null; + } + /** * A media type object that contains all the information provided on a Content-Type or Accept header */ - public class ParsedMediaType { + public static class ParsedMediaType { private final Map parameters; - private final T mediaType; + private final MediaType mediaType; - public ParsedMediaType(T mediaType, Map parameters) { + public ParsedMediaType(MediaType mediaType, Map parameters) { this.parameters = parameters; this.mediaType = mediaType; } - public T getMediaType() { + public MediaType getMediaType() { return mediaType; } @@ -125,36 +136,4 @@ public Map getParameters() { } } - public static class Builder { - private final Map formatToMediaType = new HashMap<>(); - private final Map typeWithSubtypeToMediaType = new HashMap<>(); - private final Map> parametersMap = new HashMap<>(); - - public Builder withMediaTypeAndParams(String alternativeMediaType, T mediaType, Map paramNameAndValueRegex) { - typeWithSubtypeToMediaType.put(alternativeMediaType.toLowerCase(Locale.ROOT), mediaType); - formatToMediaType.put(mediaType.format(), mediaType); - - Map parametersForMediaType = new HashMap<>(paramNameAndValueRegex.size()); - for (Map.Entry params : paramNameAndValueRegex.entrySet()) { - String parameterName = params.getKey().toLowerCase(Locale.ROOT); - String parameterRegex = params.getValue(); - Pattern pattern = Pattern.compile(parameterRegex, Pattern.CASE_INSENSITIVE); - parametersForMediaType.put(parameterName, pattern); - } - parametersMap.put(alternativeMediaType, parametersForMediaType); - - return this; - } - - public Builder copyFromMediaTypeParser(MediaTypeParser mediaTypeParser) { - formatToMediaType.putAll(mediaTypeParser.formatToMediaType); - typeWithSubtypeToMediaType.putAll(mediaTypeParser.typeWithSubtypeToMediaType); - parametersMap.putAll(mediaTypeParser.parametersMap); - return this; - } - - public MediaTypeParser build() { - return new MediaTypeParser<>(formatToMediaType, typeWithSubtypeToMediaType, parametersMap); - } - } } diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeRegistry.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeRegistry.java new file mode 100644 index 0000000000000..c6ef7ca85b74d --- /dev/null +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeRegistry.java @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.common.xcontent; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +public class MediaTypeRegistry { + + private final Map formatToMediaType; + private final Map typeWithSubtypeToMediaType; + private final Map> parametersMap; + + public MediaTypeRegistry(List definitions) { + Map formatToMediaType = new HashMap<>(); + Map typeWithSubtypeToMediaType = new HashMap<>(); + Map> parametersMap = new HashMap<>(); + definitions.forEach(definition -> { + if (definition.format() != null) { + formatToMediaType.put(definition.format(), definition.getMediaType()); + } + typeWithSubtypeToMediaType.put(definition.getMediaTypeString(), definition.getMediaType()); + Map uncompiledParams = definition.getMediaTypeParameters(); + Map parametersForMediaType = new HashMap<>(uncompiledParams.size()); + for (Entry params : uncompiledParams.entrySet()) { + String parameterName = params.getKey().toLowerCase(Locale.ROOT); + String parameterRegex = params.getValue(); + Pattern pattern = Pattern.compile(parameterRegex, Pattern.CASE_INSENSITIVE); + parametersForMediaType.put(parameterName, pattern); + } + parametersMap.put(definition.getMediaTypeString(), parametersForMediaType); + }); + + this.formatToMediaType = Map.copyOf(formatToMediaType); + this.typeWithSubtypeToMediaType = Map.copyOf(typeWithSubtypeToMediaType); + this.parametersMap = Map.copyOf(parametersMap); + } + + public MediaType formatToMediaType(String format) { + return formatToMediaType.get(format); + } + + public MediaType typeWithSubtypeToMediaType(String typeWithSubtype) { + return typeWithSubtypeToMediaType.get(typeWithSubtype); + } + + public Map parametersFor(String typeWithSubtype) { + return parametersMap.get(typeWithSubtype); + } + +} diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentType.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentType.java index 076a20bad006a..54f671f4ad9cd 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentType.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentType.java @@ -24,7 +24,7 @@ import org.elasticsearch.common.xcontent.smile.SmileXContent; import org.elasticsearch.common.xcontent.yaml.YamlXContent; -import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -114,26 +114,28 @@ public XContent xContent() { } }; - private static final String COMPATIBLE_WITH_PARAMETER_NAME = "compatible-with"; - private static final String VERSION_PATTERN = "\\d+"; - public static final MediaTypeParser mediaTypeParser = new MediaTypeParser.Builder() - .withMediaTypeAndParams("application/smile", SMILE, Collections.emptyMap()) - .withMediaTypeAndParams("application/cbor", CBOR, Collections.emptyMap()) - .withMediaTypeAndParams("application/json", JSON, Map.of("charset", "UTF-8")) - .withMediaTypeAndParams("application/yaml", YAML, Map.of("charset", "UTF-8")) - .withMediaTypeAndParams("application/*", JSON, Map.of("charset", "UTF-8")) - .withMediaTypeAndParams("application/x-ndjson", JSON, Map.of("charset", "UTF-8")) - .withMediaTypeAndParams("application/vnd.elasticsearch+json", JSON, + public static final String COMPATIBLE_WITH_PARAMETER_NAME = "compatible-with"; + public static final String VERSION_PATTERN = "\\d+"; + + public static final List MEDIA_TYPE_DEFINITIONS = List.of( + new MediaTypeDefinition(SMILE, Map.of()), + new MediaTypeDefinition(CBOR, Map.of()), + new MediaTypeDefinition(JSON, Map.of("charset", "UTF-8")), + new MediaTypeDefinition(YAML, Map.of("charset", "UTF-8")), + new MediaTypeDefinition("application/x-ndjson", JSON, Map.of("charset", "UTF-8")), + new MediaTypeDefinition("application/vnd.elasticsearch+json", JSON, + Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")), + new MediaTypeDefinition("application/vnd.elasticsearch+smile", SMILE, + Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")), + new MediaTypeDefinition("application/vnd.elasticsearch+yaml", YAML, + Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")), + new MediaTypeDefinition("application/vnd.elasticsearch+cbor", CBOR, + Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")), + new MediaTypeDefinition("application/vnd.elasticsearch+x-ndjson", JSON, Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")) - .withMediaTypeAndParams("application/vnd.elasticsearch+smile", SMILE, - Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")) - .withMediaTypeAndParams("application/vnd.elasticsearch+yaml", YAML, - Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")) - .withMediaTypeAndParams("application/vnd.elasticsearch+cbor", CBOR, - Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")) - .withMediaTypeAndParams("application/vnd.elasticsearch+x-ndjson", JSON, - Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")) - .build(); + ); + + private static final MediaTypeParser PARSER = new MediaTypeParser<>(new MediaTypeRegistry(MEDIA_TYPE_DEFINITIONS)); /** * Accepts a format string, which is most of the time is equivalent to {@link XContentType#subtype()} @@ -142,7 +144,7 @@ public XContent xContent() { * This method will return {@code null} if no match is found */ public static XContentType fromFormat(String mediaType) { - return mediaTypeParser.fromFormat(mediaType); + return PARSER.fromFormat(mediaType); } /** @@ -152,26 +154,15 @@ public static XContentType fromFormat(String mediaType) { * This method will return {@code null} if no match is found */ public static XContentType fromMediaType(String mediaTypeHeaderValue) { - return mediaTypeParser.fromMediaType(mediaTypeHeaderValue); + return PARSER.fromMediaType(mediaTypeHeaderValue); } - private int index; + private final int index; XContentType(int index) { this.index = index; } - public static Byte parseVersion(String mediaType) { - MediaTypeParser.ParsedMediaType parsedMediaType = mediaTypeParser.parseMediaType(mediaType); - if (parsedMediaType != null) { - String version = parsedMediaType - .getParameters() - .get(COMPATIBLE_WITH_PARAMETER_NAME); - return version != null ? Byte.parseByte(version) : null; - } - return null; - } - public int index() { return index; } diff --git a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/MediaTypeParserTests.java b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/MediaTypeParserTests.java index 08ca08a3d2240..9f38dfc593f7b 100644 --- a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/MediaTypeParserTests.java +++ b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/MediaTypeParserTests.java @@ -29,12 +29,8 @@ import static org.hamcrest.Matchers.nullValue; public class MediaTypeParserTests extends ESTestCase { - - MediaTypeParser mediaTypeParser = new MediaTypeParser.Builder() - .withMediaTypeAndParams("application/vnd.elasticsearch+json", - XContentType.JSON, Map.of("compatible-with", "\\d+", - "charset", "UTF-8")) - .build(); + MediaTypeRegistry mediaTypeRegistry = new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS); + MediaTypeParser mediaTypeParser = new MediaTypeParser<>(mediaTypeRegistry); public void testJsonWithParameters() throws Exception { String mediaType = "application/vnd.elasticsearch+json"; @@ -74,4 +70,26 @@ public void testInvalidParameters() { assertThat(mediaTypeParser.parseMediaType(mediaType + "; key=") , is(nullValue())); } + + public void testVersionParsing() { + byte version = (byte) Math.abs(randomByte()); + assertThat(mediaTypeParser.parseVersion("application/vnd.elasticsearch+json;compatible-with=" + version), + equalTo(version)); + assertThat(mediaTypeParser.parseVersion("application/json"), + nullValue()); + + + assertThat(mediaTypeParser.parseVersion("APPLICATION/VND.ELASTICSEARCH+JSON;COMPATIBLE-WITH=" + version), + equalTo(version)); + assertThat(mediaTypeParser.parseVersion("APPLICATION/JSON"), + nullValue()); + + assertThat(mediaTypeParser.parseVersion("application/json;compatible-with=" + version + ".0"), + is(nullValue())); + } + + public void testUnrecognizedParameter() { + assertThat(mediaTypeParser.parseVersion("application/json; sth=123"), + is(nullValue())); } + } diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java index b0010a31375c8..2fc177d5361c4 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java @@ -51,6 +51,7 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.common.xcontent.MediaTypeParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.core.internal.net.NetUtils; @@ -147,8 +148,8 @@ public class Netty4HttpServerTransport extends AbstractHttpServerTransport { public Netty4HttpServerTransport(Settings settings, NetworkService networkService, BigArrays bigArrays, ThreadPool threadPool, NamedXContentRegistry xContentRegistry, Dispatcher dispatcher, ClusterSettings clusterSettings, - SharedGroupFactory sharedGroupFactory) { - super(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher, clusterSettings); + SharedGroupFactory sharedGroupFactory, MediaTypeParser mediaTypeParser) { + super(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher, clusterSettings, mediaTypeParser); Netty4Utils.setAvailableProcessors(EsExecutors.NODE_PROCESSORS_SETTING.get(settings)); NettyAllocator.logAllocatorDescriptionIfNeeded(); this.sharedGroupFactory = sharedGroupFactory; diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/Netty4Plugin.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/Netty4Plugin.java index 1428eb7b17113..8e537582efdb1 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/Netty4Plugin.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/Netty4Plugin.java @@ -29,8 +29,10 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.xcontent.MediaTypeParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.http.HttpServerTransport.Dispatcher; import org.elasticsearch.http.netty4.Netty4HttpServerTransport; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.plugins.NetworkPlugin; @@ -84,16 +86,20 @@ public Map> getTransports(Settings settings, ThreadP } @Override - public Map> getHttpTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + public Map> getHttpTransports(Settings settings, + ThreadPool threadPool, + BigArrays bigArrays, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedXContentRegistry xContentRegistry, NetworkService networkService, - HttpServerTransport.Dispatcher dispatcher, - ClusterSettings clusterSettings) { + Dispatcher dispatcher, + ClusterSettings clusterSettings, + MediaTypeParser mediaTypeParser + ) { return Collections.singletonMap(NETTY_HTTP_TRANSPORT_NAME, () -> new Netty4HttpServerTransport(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher, - clusterSettings, getSharedGroupFactory(settings))); + clusterSettings, getSharedGroupFactory(settings), mediaTypeParser)); } private SharedGroupFactory getSharedGroupFactory(Settings settings) { diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4BadRequestTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4BadRequestTests.java index abd918f706bc7..2f0515c7fcc22 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4BadRequestTests.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4BadRequestTests.java @@ -29,6 +29,9 @@ import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.common.util.MockPageCacheRecycler; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.http.HttpTransportSettings; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; @@ -91,7 +94,7 @@ public void dispatchBadRequest(RestChannel channel, ThreadContext threadContext, Settings settings = Settings.builder().put(HttpTransportSettings.SETTING_HTTP_PORT.getKey(), getPortRange()).build(); try (HttpServerTransport httpServerTransport = new Netty4HttpServerTransport(settings, networkService, bigArrays, threadPool, xContentRegistry(), dispatcher, new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), - new SharedGroupFactory(Settings.EMPTY))) { + new SharedGroupFactory(Settings.EMPTY), new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS)))) { httpServerTransport.start(); final TransportAddress transportAddress = randomFrom(httpServerTransport.boundAddress().boundAddresses()); diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerPipeliningTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerPipeliningTests.java index a873293ab5b9f..0e9d49e64fcc4 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerPipeliningTests.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerPipeliningTests.java @@ -35,6 +35,9 @@ import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.common.util.MockPageCacheRecycler; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.http.HttpPipelinedRequest; import org.elasticsearch.http.HttpResponse; import org.elasticsearch.http.HttpServerTransport; @@ -120,7 +123,8 @@ class CustomNettyHttpServerTransport extends Netty4HttpServerTransport { Netty4HttpServerPipeliningTests.this.bigArrays, Netty4HttpServerPipeliningTests.this.threadPool, xContentRegistry(), new NullDispatcher(), new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), - new SharedGroupFactory(settings)); + new SharedGroupFactory(settings), + new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); } @Override diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java index e2ce3a6878849..b6c53a948ade0 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java @@ -57,6 +57,9 @@ import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.common.util.MockPageCacheRecycler; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.http.BindHttpException; import org.elasticsearch.http.CorsHandler; import org.elasticsearch.http.HttpServerTransport; @@ -70,8 +73,8 @@ import org.elasticsearch.test.rest.FakeRestRequest; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.SharedGroupFactory; import org.elasticsearch.transport.NettyAllocator; +import org.elasticsearch.transport.SharedGroupFactory; import org.junit.After; import org.junit.Before; @@ -101,6 +104,7 @@ public class Netty4HttpServerTransportTests extends ESTestCase { private ThreadPool threadPool; private MockBigArrays bigArrays; private ClusterSettings clusterSettings; + private MediaTypeParser mediaTypeParser; @Before public void setup() throws Exception { @@ -108,6 +112,7 @@ public void setup() throws Exception { threadPool = new TestThreadPool("test"); bigArrays = new MockBigArrays(new MockPageCacheRecycler(Settings.EMPTY), new NoneCircuitBreakerService()); clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + mediaTypeParser = new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS)); } @After @@ -174,7 +179,7 @@ public void dispatchBadRequest(RestChannel channel, ThreadContext threadContext, } }; try (Netty4HttpServerTransport transport = new Netty4HttpServerTransport(settings, networkService, bigArrays, threadPool, - xContentRegistry(), dispatcher, clusterSettings, new SharedGroupFactory(settings))) { + xContentRegistry(), dispatcher, clusterSettings, new SharedGroupFactory(settings), mediaTypeParser)) { transport.start(); final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); try (Netty4HttpClient client = new Netty4HttpClient()) { @@ -208,7 +213,7 @@ public void dispatchBadRequest(RestChannel channel, ThreadContext threadContext, public void testBindUnavailableAddress() { Settings initialSettings = createSettings(); try (Netty4HttpServerTransport transport = new Netty4HttpServerTransport(initialSettings, networkService, bigArrays, threadPool, - xContentRegistry(), new NullDispatcher(), clusterSettings, new SharedGroupFactory(Settings.EMPTY))) { + xContentRegistry(), new NullDispatcher(), clusterSettings, new SharedGroupFactory(Settings.EMPTY), mediaTypeParser)) { transport.start(); TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); Settings settings = Settings.builder() @@ -216,7 +221,7 @@ public void testBindUnavailableAddress() { .put("network.host", remoteAddress.getAddress()) .build(); try (Netty4HttpServerTransport otherTransport = new Netty4HttpServerTransport(settings, networkService, bigArrays, threadPool, - xContentRegistry(), new NullDispatcher(), clusterSettings, new SharedGroupFactory(settings))) { + xContentRegistry(), new NullDispatcher(), clusterSettings, new SharedGroupFactory(settings), mediaTypeParser)) { BindHttpException bindHttpException = expectThrows(BindHttpException.class, otherTransport::start); assertEquals( "Failed to bind to " + NetworkAddress.format(remoteAddress.address()), @@ -262,7 +267,7 @@ public void dispatchBadRequest(final RestChannel channel, final ThreadContext th try (Netty4HttpServerTransport transport = new Netty4HttpServerTransport( settings, networkService, bigArrays, threadPool, xContentRegistry(), dispatcher, clusterSettings, - new SharedGroupFactory(settings))) { + new SharedGroupFactory(settings), mediaTypeParser)) { transport.start(); final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); @@ -312,7 +317,7 @@ public void dispatchBadRequest(final RestChannel channel, final ThreadContext th try (Netty4HttpServerTransport transport = new Netty4HttpServerTransport( Settings.EMPTY, networkService, bigArrays, threadPool, xContentRegistry(), dispatcher, clusterSettings, - new SharedGroupFactory(Settings.EMPTY))) { + new SharedGroupFactory(Settings.EMPTY), mediaTypeParser)) { transport.start(); final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); @@ -372,7 +377,7 @@ public void dispatchBadRequest(final RestChannel channel, try (Netty4HttpServerTransport transport = new Netty4HttpServerTransport(settings, networkService, bigArrays, threadPool, xContentRegistry(), dispatcher, new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), - new SharedGroupFactory(settings))) { + new SharedGroupFactory(settings), mediaTypeParser)) { transport.start(); final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); @@ -435,7 +440,7 @@ public void dispatchBadRequest(final RestChannel channel, NioEventLoopGroup group = new NioEventLoopGroup(); try (Netty4HttpServerTransport transport = new Netty4HttpServerTransport(settings, networkService, bigArrays, threadPool, xContentRegistry(), dispatcher, new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), - new SharedGroupFactory(settings))) { + new SharedGroupFactory(settings), mediaTypeParser)) { transport.start(); final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerTransport.java b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerTransport.java index f10d9538d9e34..816d50ea3da1d 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerTransport.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpServerTransport.java @@ -30,6 +30,7 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.xcontent.MediaTypeParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.http.AbstractHttpServerTransport; import org.elasticsearch.http.HttpChannel; @@ -86,8 +87,9 @@ public class NioHttpServerTransport extends AbstractHttpServerTransport { public NioHttpServerTransport(Settings settings, NetworkService networkService, BigArrays bigArrays, PageCacheRecycler pageCacheRecycler, ThreadPool threadPool, NamedXContentRegistry xContentRegistry, - Dispatcher dispatcher, NioGroupFactory nioGroupFactory, ClusterSettings clusterSettings) { - super(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher, clusterSettings); + Dispatcher dispatcher, NioGroupFactory nioGroupFactory, ClusterSettings clusterSettings, + MediaTypeParser mediaTypeParser) { + super(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher, clusterSettings, mediaTypeParser); this.pageAllocator = new PageAllocator(pageCacheRecycler); this.nioGroupFactory = nioGroupFactory; diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/transport/nio/NioTransportPlugin.java b/plugins/transport-nio/src/main/java/org/elasticsearch/transport/nio/NioTransportPlugin.java index 1da90e35ba7f4..bcf2811385b99 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/transport/nio/NioTransportPlugin.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/transport/nio/NioTransportPlugin.java @@ -31,8 +31,10 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.common.xcontent.MediaTypeParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.http.HttpServerTransport.Dispatcher; import org.elasticsearch.http.nio.NioHttpServerTransport; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.plugins.NetworkPlugin; @@ -82,16 +84,20 @@ public Map> getTransports(Settings settings, ThreadP } @Override - public Map> getHttpTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + public Map> getHttpTransports(Settings settings, + ThreadPool threadPool, + BigArrays bigArrays, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedXContentRegistry xContentRegistry, NetworkService networkService, - HttpServerTransport.Dispatcher dispatcher, - ClusterSettings clusterSettings) { + Dispatcher dispatcher, + ClusterSettings clusterSettings, + MediaTypeParser mediaTypeParser + ) { return Collections.singletonMap(NIO_HTTP_TRANSPORT_NAME, () -> new NioHttpServerTransport(settings, networkService, bigArrays, pageCacheRecycler, threadPool, xContentRegistry, - dispatcher, getNioGroupFactory(settings), clusterSettings)); + dispatcher, getNioGroupFactory(settings), clusterSettings, mediaTypeParser)); } private synchronized NioGroupFactory getNioGroupFactory(Settings settings) { diff --git a/plugins/transport-nio/src/test/java/org/elasticsearch/http/nio/NioHttpServerTransportTests.java b/plugins/transport-nio/src/test/java/org/elasticsearch/http/nio/NioHttpServerTransportTests.java index d76864bfb5be2..f7e61abc61157 100644 --- a/plugins/transport-nio/src/test/java/org/elasticsearch/http/nio/NioHttpServerTransportTests.java +++ b/plugins/transport-nio/src/test/java/org/elasticsearch/http/nio/NioHttpServerTransportTests.java @@ -45,6 +45,9 @@ import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.common.util.MockPageCacheRecycler; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.http.BindHttpException; import org.elasticsearch.http.CorsHandler; import org.elasticsearch.http.HttpServerTransport; @@ -89,6 +92,7 @@ public class NioHttpServerTransportTests extends ESTestCase { private ThreadPool threadPool; private MockBigArrays bigArrays; private MockPageCacheRecycler pageRecycler; + private MediaTypeParser mediaTypeParser; @Before public void setup() throws Exception { @@ -96,6 +100,7 @@ public void setup() throws Exception { threadPool = new TestThreadPool("test"); pageRecycler = new MockPageCacheRecycler(Settings.EMPTY); bigArrays = new MockBigArrays(pageRecycler, new NoneCircuitBreakerService()); + mediaTypeParser = new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS)); } @After @@ -162,7 +167,7 @@ public void dispatchBadRequest(RestChannel channel, ThreadContext threadContext, }; try (NioHttpServerTransport transport = new NioHttpServerTransport(settings, networkService, bigArrays, pageRecycler, threadPool, xContentRegistry(), dispatcher, new NioGroupFactory(settings, logger), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS))) { + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser)) { transport.start(); final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); try (NioHttpClient client = new NioHttpClient()) { @@ -197,7 +202,7 @@ public void testBindUnavailableAddress() { final Settings initialSettings = createSettings(); try (NioHttpServerTransport transport = new NioHttpServerTransport(initialSettings, networkService, bigArrays, pageRecycler, threadPool, xContentRegistry(), new NullDispatcher(), new NioGroupFactory(Settings.EMPTY, logger), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS))) { + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser)) { transport.start(); TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); Settings settings = Settings.builder() @@ -206,7 +211,7 @@ threadPool, xContentRegistry(), new NullDispatcher(), new NioGroupFactory(Settin .build(); try (NioHttpServerTransport otherTransport = new NioHttpServerTransport(settings, networkService, bigArrays, pageRecycler, threadPool, xContentRegistry(), new NullDispatcher(), new NioGroupFactory(Settings.EMPTY, logger), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS))) { + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser)) { BindHttpException bindHttpException = expectThrows(BindHttpException.class, () -> otherTransport.start()); assertEquals( "Failed to bind to " + NetworkAddress.format(remoteAddress.address()), @@ -243,7 +248,7 @@ public void dispatchBadRequest(final RestChannel channel, try (NioHttpServerTransport transport = new NioHttpServerTransport(settings, networkService, bigArrays, pageRecycler, threadPool, xContentRegistry(), dispatcher, new NioGroupFactory(settings, logger), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS))) { + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser)) { transport.start(); final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); @@ -305,7 +310,8 @@ public void dispatchBadRequest(final RestChannel channel, final ThreadContext th try (NioHttpServerTransport transport = new NioHttpServerTransport( Settings.EMPTY, networkService, bigArrays, pageRecycler, threadPool, xContentRegistry(), dispatcher, - new NioGroupFactory(Settings.EMPTY, logger), new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS))) { + new NioGroupFactory(Settings.EMPTY, logger), new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + mediaTypeParser)) { transport.start(); final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); @@ -361,7 +367,7 @@ public void dispatchBadRequest(final RestChannel channel, final ThreadContext th try (NioHttpServerTransport transport = new NioHttpServerTransport(settings, networkService, bigArrays, pageRecycler, threadPool, xContentRegistry(), dispatcher, new NioGroupFactory(settings, logger), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS))) { + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser)) { transport.start(); final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); @@ -411,7 +417,7 @@ public void dispatchBadRequest(final RestChannel channel, try (NioHttpServerTransport transport = new NioHttpServerTransport(settings, networkService, bigArrays, pageRecycler, threadPool, xContentRegistry(), dispatcher, new NioGroupFactory(settings, logger), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS))) { + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser)) { transport.start(); final TransportAddress remoteAddress = randomFrom(transport.boundAddress().boundAddresses()); diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 3956b29d13b78..eb06509293c94 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -246,6 +246,7 @@ import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; import org.elasticsearch.gateway.TransportNodesListGatewayMetaState; import org.elasticsearch.gateway.TransportNodesListGatewayStartedShards; import org.elasticsearch.index.seqno.GlobalCheckpointSyncAction; @@ -461,7 +462,8 @@ public ActionModule(Settings settings, IndexNameExpressionResolver indexNameExpr indicesAliasesRequestRequestValidators = new RequestValidators<>( actionPlugins.stream().flatMap(p -> p.indicesAliasesRequestValidators().stream()).collect(Collectors.toList())); - restController = new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService, compatibleVersion); + restController = + new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService, compatibleVersion); } public Map> getActions() { diff --git a/server/src/main/java/org/elasticsearch/common/network/NetworkModule.java b/server/src/main/java/org/elasticsearch/common/network/NetworkModule.java index 870d63ed5e57c..2fe581996291f 100644 --- a/server/src/main/java/org/elasticsearch/common/network/NetworkModule.java +++ b/server/src/main/java/org/elasticsearch/common/network/NetworkModule.java @@ -36,6 +36,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.xcontent.MediaTypeParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.http.HttpServerTransport; @@ -114,11 +115,12 @@ public NetworkModule(Settings settings, List plugins, ThreadPool NamedWriteableRegistry namedWriteableRegistry, NamedXContentRegistry xContentRegistry, NetworkService networkService, HttpServerTransport.Dispatcher dispatcher, - ClusterSettings clusterSettings) { + ClusterSettings clusterSettings, MediaTypeParser mediaTypeParser) { this.settings = settings; for (NetworkPlugin plugin : plugins) { Map> httpTransportFactory = plugin.getHttpTransports(settings, threadPool, bigArrays, - pageCacheRecycler, circuitBreakerService, xContentRegistry, networkService, dispatcher, clusterSettings); + pageCacheRecycler, circuitBreakerService, xContentRegistry, networkService, dispatcher, clusterSettings, mediaTypeParser + ); for (Map.Entry> entry : httpTransportFactory.entrySet()) { registerHttpTransport(entry.getKey(), entry.getValue()); } diff --git a/server/src/main/java/org/elasticsearch/http/AbstractHttpServerTransport.java b/server/src/main/java/org/elasticsearch/http/AbstractHttpServerTransport.java index af8095d6dece1..0dc952b18dbc1 100644 --- a/server/src/main/java/org/elasticsearch/http/AbstractHttpServerTransport.java +++ b/server/src/main/java/org/elasticsearch/http/AbstractHttpServerTransport.java @@ -40,6 +40,7 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.MediaTypeParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.rest.RestChannel; import org.elasticsearch.rest.RestRequest; @@ -77,6 +78,7 @@ public abstract class AbstractHttpServerTransport extends AbstractLifecycleCompo protected final Dispatcher dispatcher; protected final CorsHandler corsHandler; private final NamedXContentRegistry xContentRegistry; + private final MediaTypeParser mediaTypeParser; protected final PortsRange port; protected final ByteSizeValue maxContentLength; @@ -91,12 +93,14 @@ public abstract class AbstractHttpServerTransport extends AbstractLifecycleCompo private final HttpTracer tracer; protected AbstractHttpServerTransport(Settings settings, NetworkService networkService, BigArrays bigArrays, ThreadPool threadPool, - NamedXContentRegistry xContentRegistry, Dispatcher dispatcher, ClusterSettings clusterSettings) { + NamedXContentRegistry xContentRegistry, Dispatcher dispatcher, ClusterSettings clusterSettings, + MediaTypeParser mediaTypeParser) { this.settings = settings; this.networkService = networkService; this.bigArrays = bigArrays; this.threadPool = threadPool; this.xContentRegistry = xContentRegistry; + this.mediaTypeParser = mediaTypeParser; this.dispatcher = dispatcher; this.handlingSettings = HttpHandlingSettings.fromSettings(settings); this.corsHandler = CorsHandler.fromSettings(settings); @@ -344,13 +348,13 @@ private void handleIncomingRequest(final HttpRequest httpRequest, final HttpChan { RestRequest innerRestRequest; try { - innerRestRequest = RestRequest.request(xContentRegistry, httpRequest, httpChannel); + innerRestRequest = RestRequest.request(xContentRegistry, httpRequest, httpChannel, mediaTypeParser); } catch (final RestRequest.ContentTypeHeaderException e) { badRequestCause = ExceptionsHelper.useOrSuppress(badRequestCause, e); innerRestRequest = requestWithoutContentTypeHeader(httpRequest, httpChannel, badRequestCause); } catch (final RestRequest.BadParameterException e) { badRequestCause = ExceptionsHelper.useOrSuppress(badRequestCause, e); - innerRestRequest = RestRequest.requestWithoutParameters(xContentRegistry, httpRequest, httpChannel); + innerRestRequest = RestRequest.requestWithoutParameters(xContentRegistry, httpRequest, httpChannel, mediaTypeParser); } restRequest = innerRestRequest; } @@ -373,7 +377,8 @@ private void handleIncomingRequest(final HttpRequest httpRequest, final HttpChan trace); } catch (final IllegalArgumentException e) { badRequestCause = ExceptionsHelper.useOrSuppress(badRequestCause, e); - final RestRequest innerRequest = RestRequest.requestWithoutParameters(xContentRegistry, httpRequest, httpChannel); + final RestRequest innerRequest = + RestRequest.requestWithoutParameters(xContentRegistry, httpRequest, httpChannel, mediaTypeParser); innerChannel = new DefaultRestChannel(httpChannel, httpRequest, innerRequest, bigArrays, handlingSettings, threadContext, corsHandler, trace); @@ -387,10 +392,10 @@ private void handleIncomingRequest(final HttpRequest httpRequest, final HttpChan private RestRequest requestWithoutContentTypeHeader(HttpRequest httpRequest, HttpChannel httpChannel, Exception badRequestCause) { HttpRequest httpRequestWithoutContentType = httpRequest.removeHeader("Content-Type"); try { - return RestRequest.request(xContentRegistry, httpRequestWithoutContentType, httpChannel); + return RestRequest.request(xContentRegistry, httpRequestWithoutContentType, httpChannel, mediaTypeParser); } catch (final RestRequest.BadParameterException e) { badRequestCause.addSuppressed(e); - return RestRequest.requestWithoutParameters(xContentRegistry, httpRequestWithoutContentType, httpChannel); + return RestRequest.requestWithoutParameters(xContentRegistry, httpRequestWithoutContentType, httpChannel, mediaTypeParser); } } diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 15ca8e4f66bbd..cfa7637609a32 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -87,7 +87,11 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.xcontent.MediaTypeDefinition; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.discovery.Discovery; import org.elasticsearch.discovery.DiscoveryModule; @@ -330,6 +334,7 @@ protected Node(final Environment initialEnvironment, .collect(Collectors.toSet()); DiscoveryNode.setAdditionalRoles(additionalRoles); + /* * Create the environment based on the finalized view of the settings. This is to ensure that components get the same setting * values, no matter they ask for them from. @@ -529,16 +534,26 @@ protected Node(final Environment initialEnvironment, repositoriesServiceReference::get).stream()) .collect(Collectors.toList()); + List mediaTypeDefinitions = pluginsService.filterPlugins(ActionPlugin.class) + .stream() + .flatMap(plugin -> plugin.getAdditionalMediaTypes().stream()) + .collect(toList()); + mediaTypeDefinitions.addAll(XContentType.MEDIA_TYPE_DEFINITIONS); + + MediaTypeRegistry globalMediaTypeRegistry = new MediaTypeRegistry(mediaTypeDefinitions); + + CompatibleVersion restCompatibleFunction = getRestCompatibleFunction(); + ActionModule actionModule = new ActionModule(settings, clusterModule.getIndexNameExpressionResolver(), settingsModule.getIndexScopedSettings(), settingsModule.getClusterSettings(), settingsModule.getSettingsFilter(), threadPool, pluginsService.filterPlugins(ActionPlugin.class), client, circuitBreakerService, usageService, - systemIndices, getRestCompatibleFunction()); + systemIndices, restCompatibleFunction); modules.add(actionModule); final RestController restController = actionModule.getRestController(); final NetworkModule networkModule = new NetworkModule(settings, pluginsService.filterPlugins(NetworkPlugin.class), threadPool, bigArrays, pageCacheRecycler, circuitBreakerService, namedWriteableRegistry, xContentRegistry, - networkService, restController, clusterService.getClusterSettings()); + networkService, restController, clusterService.getClusterSettings(), new MediaTypeParser<>(globalMediaTypeRegistry)); Collection>> indexTemplateMetadataUpgraders = pluginsService.filterPlugins(Plugin.class).stream() .map(Plugin::getIndexTemplateMetadataUpgrader) diff --git a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java index ef9861f266222..c7e37ba8c4c6d 100644 --- a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java @@ -19,9 +19,9 @@ package org.elasticsearch.plugins; -import org.elasticsearch.action.ActionType; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.ActionType; import org.elasticsearch.action.RequestValidators; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; @@ -34,6 +34,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.MediaTypeDefinition; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; import org.elasticsearch.rest.RestHeaderDefinition; @@ -181,4 +182,7 @@ default Collection> in return Collections.emptyList(); } + default List getAdditionalMediaTypes() { + return List.of(); + } } diff --git a/server/src/main/java/org/elasticsearch/plugins/NetworkPlugin.java b/server/src/main/java/org/elasticsearch/plugins/NetworkPlugin.java index a7c9e7bd842b7..d5e907e1a0288 100644 --- a/server/src/main/java/org/elasticsearch/plugins/NetworkPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/NetworkPlugin.java @@ -18,11 +18,6 @@ */ package org.elasticsearch.plugins; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; - import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; @@ -30,13 +25,20 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.MediaTypeParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.http.HttpServerTransport.Dispatcher; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportInterceptor; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + /** * Plugin for extending network and transport related classes */ @@ -69,13 +71,16 @@ default Map> getTransports(Settings settings, Thread * Returns a map of {@link HttpServerTransport} suppliers. * See {@link org.elasticsearch.common.network.NetworkModule#HTTP_TYPE_SETTING} to configure a specific implementation. */ - default Map> getHttpTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + default Map> getHttpTransports(Settings settings, + ThreadPool threadPool, + BigArrays bigArrays, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedXContentRegistry xContentRegistry, NetworkService networkService, - HttpServerTransport.Dispatcher dispatcher, - ClusterSettings clusterSettings) { + Dispatcher dispatcher, + ClusterSettings clusterSettings, + MediaTypeParser mediaTypeParser) { return Collections.emptyMap(); } } diff --git a/server/src/main/java/org/elasticsearch/plugins/RestCompatibilityPlugin.java b/server/src/main/java/org/elasticsearch/plugins/RestCompatibilityPlugin.java index 9fd73a24ee87b..c5e4fcbe60059 100644 --- a/server/src/main/java/org/elasticsearch/plugins/RestCompatibilityPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/RestCompatibilityPlugin.java @@ -21,6 +21,7 @@ import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.xcontent.MediaTypeParser.ParsedMediaType; /** @@ -35,5 +36,6 @@ public interface RestCompatibilityPlugin { * @param hasContent - a flag indicating if a request has content * @return a requested Compatible API Version */ - Version getCompatibleVersion(@Nullable String acceptHeader, @Nullable String contentTypeHeader, boolean hasContent); + Version getCompatibleVersion(@Nullable ParsedMediaType acceptHeader, @Nullable ParsedMediaType contentTypeHeader, boolean hasContent); + } diff --git a/server/src/main/java/org/elasticsearch/rest/AbstractRestChannel.java b/server/src/main/java/org/elasticsearch/rest/AbstractRestChannel.java index 6f5aa618ae4d0..8cd1ea1be29ef 100644 --- a/server/src/main/java/org/elasticsearch/rest/AbstractRestChannel.java +++ b/server/src/main/java/org/elasticsearch/rest/AbstractRestChannel.java @@ -101,11 +101,12 @@ public XContentBuilder newBuilder(@Nullable XContentType requestContentType, boo public XContentBuilder newBuilder(@Nullable XContentType requestContentType, @Nullable XContentType responseContentType, boolean useFiltering) throws IOException { if (responseContentType == null) { - if (Strings.hasText(format)) { - responseContentType = XContentType.fromFormat(format); + if (request.getFormatMediaType() != null && request.getFormatMediaType() instanceof XContentType) { + responseContentType = (XContentType) request.getFormatMediaType(); } - if (responseContentType == null) { - responseContentType = XContentType.fromMediaType(acceptHeader); + if (responseContentType == null && request.getAcceptMediaType() != null && + request.getAcceptMediaType().getMediaType() instanceof XContentType) { + responseContentType = (XContentType) request.getAcceptMediaType().getMediaType(); } } // try to determine the response content type from the media type or the format query string parameter, with the format parameter diff --git a/server/src/main/java/org/elasticsearch/rest/CompatibleVersion.java b/server/src/main/java/org/elasticsearch/rest/CompatibleVersion.java index 48ef5a8f8a87e..1f7461ea65f5b 100644 --- a/server/src/main/java/org/elasticsearch/rest/CompatibleVersion.java +++ b/server/src/main/java/org/elasticsearch/rest/CompatibleVersion.java @@ -21,6 +21,7 @@ import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.xcontent.MediaTypeParser.ParsedMediaType; /** * An interface used to specify a function that returns a compatible API version @@ -28,7 +29,7 @@ */ @FunctionalInterface public interface CompatibleVersion { - Version get(@Nullable String acceptHeader, @Nullable String contentTypeHeader, boolean hasContent); + Version get(@Nullable ParsedMediaType acceptMediaType, @Nullable ParsedMediaType contentTypeHeader, boolean hasContent); CompatibleVersion CURRENT_VERSION = (acceptHeader, contentTypeHeader, hasContent) -> Version.CURRENT; } diff --git a/server/src/main/java/org/elasticsearch/rest/RestController.java b/server/src/main/java/org/elasticsearch/rest/RestController.java index 5f404100939eb..0484c9089633d 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestController.java +++ b/server/src/main/java/org/elasticsearch/rest/RestController.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.path.PathTrie; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.MediaTypeParser.ParsedMediaType; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.core.internal.io.Streams; @@ -91,7 +92,7 @@ public class RestController implements HttpServerTransport.Dispatcher { /** Rest headers that are copied to internal requests made during a rest request. */ private final Set headersToCopy; private final UsageService usageService; - private CompatibleVersion compatibleVersion; + private final CompatibleVersion compatibleVersion; public RestController(Set headersToCopy, UnaryOperator handlerWrapper, @@ -229,14 +230,16 @@ private void dispatchRequest(RestRequest request, RestChannel channel, RestHandl throws Exception { final int contentLength = request.contentLength(); if (contentLength > 0) { - final XContentType xContentType = request.getXContentType(); - if (xContentType == null) { + final ParsedMediaType parsedMediaType = request.getContentType(); + if (parsedMediaType == null) { sendContentTypeErrorMessage(request.getAllHeaderValues("Content-Type"), channel); return; } - if (handler.supportsContentStream() && xContentType != XContentType.JSON && xContentType != XContentType.SMILE) { + if (handler.supportsContentStream() && parsedMediaType.getMediaType() != XContentType.JSON && + parsedMediaType.getMediaType() != XContentType.SMILE) { channel.sendResponse(BytesRestResponse.createSimpleErrorResponse(channel, RestStatus.NOT_ACCEPTABLE, - "Content-Type [" + xContentType + "] does not support stream parsing. Use JSON or SMILE instead")); + "Content-Type [" + parsedMediaType.getMediaType().typeWithSubtype() + "] does not support stream parsing. " + + "Use JSON or SMILE instead")); return; } } @@ -324,8 +327,8 @@ private void tryAllHandlers(final RestRequest request, final RestChannel channel final String rawPath = request.rawPath(); final String uri = request.uri(); final RestRequest.Method requestMethod; - //TODO: USAGE_1 now that we have a version we can implement a REST handler that accepts path, method AND version - Version version = compatibleVersion.get(request.header("Accept"), request.header("Content-Type"), request.hasContent()); + + Version version = compatibleVersion.get(request.getContentType(), request.getContentType(), request.hasContent()); try { // Resolves the HTTP method and fails if the method is invalid diff --git a/server/src/main/java/org/elasticsearch/rest/RestRequest.java b/server/src/main/java/org/elasticsearch/rest/RestRequest.java index 83f1a89798531..1197e5d5f4e44 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestRequest.java +++ b/server/src/main/java/org/elasticsearch/rest/RestRequest.java @@ -19,7 +19,6 @@ package org.elasticsearch.rest; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.Booleans; import org.elasticsearch.common.CheckedConsumer; @@ -31,6 +30,9 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.MediaType; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeParser.ParsedMediaType; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentParser; @@ -48,7 +50,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; -import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.elasticsearch.common.unit.ByteSizeValue.parseBytesSizeValue; @@ -56,9 +57,6 @@ public class RestRequest implements ToXContent.Params { - // tchar pattern as defined by RFC7230 section 3.2.6 - private static final Pattern TCHAR_PATTERN = Pattern.compile("[a-zA-z0-9!#$%&'*+\\-.\\^_`|~]+"); - private static final AtomicLong requestIdGenerator = new AtomicLong(); private final NamedXContentRegistry xContentRegistry; @@ -66,8 +64,14 @@ public class RestRequest implements ToXContent.Params { private final Map> headers; private final String rawPath; private final Set consumedParams = new HashSet<>(); - private final SetOnce xContentType = new SetOnce<>(); private final HttpChannel httpChannel; + private final MediaTypeParser mediaTypeParser; + @Nullable + private final ParsedMediaType contentType; + @Nullable + private final ParsedMediaType acceptMediaType; + @Nullable + private final MediaType formatMediaType; private HttpRequest httpRequest; @@ -81,27 +85,21 @@ public boolean isContentConsumed() { // for testing protected RestRequest(NamedXContentRegistry xContentRegistry, Map params, String path, - Map> headers, HttpRequest httpRequest, HttpChannel httpChannel) { - this(xContentRegistry, params, path, headers, httpRequest, httpChannel, requestIdGenerator.incrementAndGet()); + Map> headers, HttpRequest httpRequest, HttpChannel httpChannel, + MediaTypeParser mediaTypeParser) { + this(xContentRegistry, params, path, headers, httpRequest, httpChannel, mediaTypeParser, requestIdGenerator.incrementAndGet()); } protected RestRequest(RestRequest restRequest) { this(restRequest.getXContentRegistry(), restRequest.params(), restRequest.path(), restRequest.getHeaders(), - restRequest.getHttpRequest(), restRequest.getHttpChannel(), restRequest.getRequestId()); + restRequest.getHttpRequest(), restRequest.getHttpChannel(), restRequest.mediaTypeParser, restRequest.getRequestId()); } private RestRequest(NamedXContentRegistry xContentRegistry, Map params, String path, - Map> headers, HttpRequest httpRequest, HttpChannel httpChannel, long requestId) { - final XContentType xContentType; - try { - xContentType = parseContentType(headers.get("Content-Type")); - } catch (final IllegalArgumentException e) { - throw new ContentTypeHeaderException(e); - } - if (xContentType != null) { - this.xContentType.set(xContentType); - } + Map> headers, HttpRequest httpRequest, HttpChannel httpChannel, + MediaTypeParser mediaTypeParser, long requestId) { + this.mediaTypeParser = mediaTypeParser; this.xContentRegistry = xContentRegistry; this.httpRequest = httpRequest; this.httpChannel = httpChannel; @@ -109,6 +107,9 @@ private RestRequest(NamedXContentRegistry xContentRegistry, Map this.rawPath = path; this.headers = Collections.unmodifiableMap(headers); this.requestId = requestId; + this.acceptMediaType = mediaTypeParser.parseMediaType(header("Accept")); + this.contentType = mediaTypeParser.parseMediaType(header("Content-Type")); + this.formatMediaType = mediaTypeParser.fromFormat(param("format")); } @@ -132,10 +133,11 @@ void ensureSafeBuffers() { * @throws BadParameterException if the parameters can not be decoded * @throws ContentTypeHeaderException if the Content-Type header can not be parsed */ - public static RestRequest request(NamedXContentRegistry xContentRegistry, HttpRequest httpRequest, HttpChannel httpChannel) { + public static RestRequest request(NamedXContentRegistry xContentRegistry, HttpRequest httpRequest, HttpChannel httpChannel, + MediaTypeParser mediaTypeParser) { Map params = params(httpRequest.uri()); String path = path(httpRequest.uri()); - return new RestRequest(xContentRegistry, params, path, httpRequest.getHeaders(), httpRequest, httpChannel, + return new RestRequest(xContentRegistry, params, path, httpRequest.getHeaders(), httpRequest, httpChannel, mediaTypeParser, requestIdGenerator.incrementAndGet()); } @@ -171,10 +173,10 @@ private static String path(final String uri) { * @throws ContentTypeHeaderException if the Content-Type header can not be parsed */ public static RestRequest requestWithoutParameters(NamedXContentRegistry xContentRegistry, HttpRequest httpRequest, - HttpChannel httpChannel) { + HttpChannel httpChannel, MediaTypeParser mediaTypeParser) { Map params = Collections.emptyMap(); return new RestRequest(xContentRegistry, params, httpRequest.uri(), httpRequest.getHeaders(), httpRequest, httpChannel, - requestIdGenerator.incrementAndGet()); + mediaTypeParser, requestIdGenerator.incrementAndGet()); } public enum Method { @@ -231,7 +233,7 @@ public BytesReference content() { public final BytesReference requiredContent() { if (hasContent() == false) { throw new ElasticsearchParseException("request body is required"); - } else if (xContentType.get() == null) { + } else if (contentType == null) { throw new IllegalStateException("unknown content type"); } return content(); @@ -277,7 +279,29 @@ public final long getRequestId() { */ @Nullable public final XContentType getXContentType() { - return xContentType.get(); + if (contentType != null) { + if (contentType.getMediaType() instanceof XContentType) { + return (XContentType) contentType.getMediaType(); + } else { + throw new IllegalStateException("content type [" + contentType.getMediaType().typeWithSubtype() + "] is not xcontent"); + } + } + return null; + } + + @Nullable + public final ParsedMediaType getContentType() { + return contentType; + } + + @Nullable + public final ParsedMediaType getAcceptMediaType() { + return acceptMediaType; + } + + @Nullable + public final MediaType getFormatMediaType() { + return formatMediaType; } public HttpChannel getHttpChannel() { @@ -426,7 +450,12 @@ public NamedXContentRegistry getXContentRegistry() { */ public final XContentParser contentParser() throws IOException { BytesReference content = requiredContent(); // will throw exception if body or content type missing - return xContentType.get().xContent().createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, content.streamInput()); + if (contentType.getMediaType() instanceof XContentType) { + return ((XContentType) contentType.getMediaType()).xContent() + .createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, content.streamInput()); + } else { + throw new IllegalStateException("content type [" + contentType.getMediaType().typeWithSubtype() + "] is not xcontent"); + } } /** @@ -486,7 +515,11 @@ public final Tuple contentOrSourceParam() { if (hasContentOrSourceParam() == false) { throw new ElasticsearchParseException("request body or source parameter is required"); } else if (hasContent()) { - return new Tuple<>(xContentType.get(), requiredContent()); + if (contentType.getMediaType() instanceof XContentType) { + return new Tuple<>((XContentType) contentType.getMediaType(), requiredContent()); + } else { + throw new IllegalStateException("content type [" + contentType.getMediaType().typeWithSubtype() + "] is not xcontent"); + } } String source = param("source"); String typeParam = param("source_content_type"); @@ -505,7 +538,7 @@ public final Tuple contentOrSourceParam() { * Parses the given content type string for the media type. This method currently ignores parameters. */ // TODO stop ignoring parameters such as charset... - public static XContentType parseContentType(List header) { + public XContentType parseContentType(List header) { if (header == null || header.isEmpty()) { return null; } else if (header.size() > 1) { @@ -513,17 +546,16 @@ public static XContentType parseContentType(List header) { } String rawContentType = header.get(0); - final String[] elements = rawContentType.split("[ \t]*;"); - if (elements.length > 0) { - final String[] splitMediaType = elements[0].split("/"); - if (splitMediaType.length == 2 && TCHAR_PATTERN.matcher(splitMediaType[0]).matches() - && TCHAR_PATTERN.matcher(splitMediaType[1].trim()).matches()) { - return XContentType.fromMediaType(elements[0]); + ParsedMediaType parsedMediaType = mediaTypeParser.parseMediaType(rawContentType); + if (parsedMediaType != null) { + if (parsedMediaType.getMediaType() instanceof XContentType) { + return (XContentType) parsedMediaType.getMediaType(); } else { - throw new IllegalArgumentException("invalid Content-Type header [" + rawContentType + "]"); + throw new IllegalArgumentException("content type [" + contentType.getMediaType().typeWithSubtype() + "] is not xcontent"); } + } else { + throw new IllegalArgumentException("unsupported Content-Type header [" + rawContentType + "]"); } - throw new IllegalArgumentException("empty Content-Type header"); } public static class ContentTypeHeaderException extends RuntimeException { diff --git a/server/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java b/server/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java index bbb746baafe92..b287bd791b4f8 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java +++ b/server/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java @@ -59,10 +59,11 @@ public static RestResponse buildResponse(Table table, RestChannel channel) throw } private static XContentType getXContentType(RestRequest request) { - if (request.hasParam("format")) { - return XContentType.fromFormat(request.param("format")); + if (request.getFormatMediaType() != null && request.getFormatMediaType() instanceof XContentType) { + return (XContentType) request.getFormatMediaType(); } - return XContentType.fromMediaType(request.header("Accept")); + return request.getAcceptMediaType() != null && request.getAcceptMediaType().getMediaType() instanceof XContentType ? + (XContentType) request.getAcceptMediaType().getMediaType() : null; } public static RestResponse buildXContentBuilder(Table table, RestChannel channel) throws Exception { diff --git a/server/src/test/java/org/elasticsearch/common/network/NetworkModuleTests.java b/server/src/test/java/org/elasticsearch/common/network/NetworkModuleTests.java index 43fd669f71da3..abb4582f2deab 100644 --- a/server/src/test/java/org/elasticsearch/common/network/NetworkModuleTests.java +++ b/server/src/test/java/org/elasticsearch/common/network/NetworkModuleTests.java @@ -27,9 +27,13 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.http.HttpInfo; import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.http.HttpServerTransport.Dispatcher; import org.elasticsearch.http.HttpStats; import org.elasticsearch.http.NullDispatcher; import org.elasticsearch.indices.breaker.CircuitBreakerService; @@ -112,14 +116,17 @@ public void testRegisterHttpTransport() { NetworkModule module = newNetworkModule(settings, new NetworkPlugin() { @Override - public Map> getHttpTransports(Settings settings, ThreadPool threadPool, + public Map> getHttpTransports(Settings settings, + ThreadPool threadPool, BigArrays bigArrays, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedXContentRegistry xContentRegistry, NetworkService networkService, - HttpServerTransport.Dispatcher requestDispatcher, - ClusterSettings clusterSettings) { + Dispatcher requestDispatcher, + ClusterSettings clusterSettings, + MediaTypeParser mediaTypeParser + ) { return Collections.singletonMap("custom", custom); } }); @@ -150,14 +157,17 @@ public Map> getTransports(Settings settings, ThreadP } @Override - public Map> getHttpTransports(Settings settings, ThreadPool threadPool, + public Map> getHttpTransports(Settings settings, + ThreadPool threadPool, BigArrays bigArrays, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedXContentRegistry xContentRegistry, NetworkService networkService, - HttpServerTransport.Dispatcher requestDispatcher, - ClusterSettings clusterSettings) { + Dispatcher requestDispatcher, + ClusterSettings clusterSettings, + MediaTypeParser mediaTypeParser + ) { Map> supplierMap = new HashMap<>(); supplierMap.put("custom", custom); supplierMap.put("default_custom", def); @@ -186,14 +196,17 @@ public Map> getTransports(Settings settings, ThreadP } @Override - public Map> getHttpTransports(Settings settings, ThreadPool threadPool, + public Map> getHttpTransports(Settings settings, + ThreadPool threadPool, BigArrays bigArrays, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedXContentRegistry xContentRegistry, NetworkService networkService, - HttpServerTransport.Dispatcher requestDispatcher, - ClusterSettings clusterSettings) { + Dispatcher requestDispatcher, + ClusterSettings clusterSettings, + MediaTypeParser mediaTypeParser + ) { Map> supplierMap = new HashMap<>(); supplierMap.put("custom", custom); supplierMap.put("default_custom", def); @@ -259,6 +272,7 @@ public List getTransportInterceptors(NamedWriteableRegistr private NetworkModule newNetworkModule(Settings settings, NetworkPlugin... plugins) { return new NetworkModule(settings, Arrays.asList(plugins), threadPool, null, null, null, null, xContentRegistry(), null, new NullDispatcher(), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); } } diff --git a/server/src/test/java/org/elasticsearch/common/xcontent/XContentTypeTests.java b/server/src/test/java/org/elasticsearch/common/xcontent/XContentTypeTests.java index fa937f84d615c..6bc8ed29f15e7 100644 --- a/server/src/test/java/org/elasticsearch/common/xcontent/XContentTypeTests.java +++ b/server/src/test/java/org/elasticsearch/common/xcontent/XContentTypeTests.java @@ -23,7 +23,6 @@ import java.util.Locale; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; public class XContentTypeTests extends ESTestCase { @@ -118,33 +117,6 @@ public void testVersionedMediaType() { equalTo(XContentType.JSON)); } - public void testVersionParsing() { - byte version = (byte) Math.abs(randomByte()); - assertThat(XContentType.parseVersion("application/vnd.elasticsearch+json;compatible-with=" + version), - equalTo(version)); - assertThat(XContentType.parseVersion("application/vnd.elasticsearch+cbor;compatible-with=" + version), - equalTo(version)); - assertThat(XContentType.parseVersion("application/vnd.elasticsearch+smile;compatible-with=" + version), - equalTo(version)); - assertThat(XContentType.parseVersion("application/vnd.elasticsearch+x-ndjson;compatible-with=" + version), - equalTo(version)); - assertThat(XContentType.parseVersion("application/json"), - nullValue()); - - - assertThat(XContentType.parseVersion("APPLICATION/VND.ELASTICSEARCH+JSON;COMPATIBLE-WITH=" + version), - equalTo(version)); - assertThat(XContentType.parseVersion("APPLICATION/JSON"), - nullValue()); - - assertThat(XContentType.parseVersion("application/json;compatible-with=" + version + ".0"), - is(nullValue())); - } - - public void testUnrecognizedParameter() { - assertThat(XContentType.parseVersion("application/json; sth=123"), - is(nullValue())); } - public void testMediaTypeWithoutESSubtype() { String version = String.valueOf(Math.abs(randomByte())); assertThat(XContentType.fromMediaType("application/json;compatible-with=" + version), nullValue()); diff --git a/server/src/test/java/org/elasticsearch/http/AbstractHttpServerTransportTests.java b/server/src/test/java/org/elasticsearch/http/AbstractHttpServerTransportTests.java index 348e8a83af731..2ec7a39d514f5 100644 --- a/server/src/test/java/org/elasticsearch/http/AbstractHttpServerTransportTests.java +++ b/server/src/test/java/org/elasticsearch/http/AbstractHttpServerTransportTests.java @@ -33,7 +33,10 @@ import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.common.util.MockPageCacheRecycler; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.rest.RestChannel; import org.elasticsearch.rest.RestRequest; @@ -140,7 +143,8 @@ public void dispatchBadRequest(final RestChannel channel, try (AbstractHttpServerTransport transport = new AbstractHttpServerTransport(Settings.EMPTY, networkService, bigArrays, threadPool, xContentRegistry(), dispatcher, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)) { + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))) { @Override protected HttpServerChannel bind(InetSocketAddress hostAddress) { @@ -199,7 +203,7 @@ public void dispatchRequest(RestRequest request, RestChannel channel, ThreadCont public void dispatchBadRequest(RestChannel channel, ThreadContext threadContext, Throwable cause) { channel.sendResponse(emptyResponse(RestStatus.BAD_REQUEST)); } - }, clusterSettings) { + }, clusterSettings, new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))) { @Override protected HttpServerChannel bind(InetSocketAddress hostAddress) { return null; diff --git a/server/src/test/java/org/elasticsearch/http/DefaultRestChannelTests.java b/server/src/test/java/org/elasticsearch/http/DefaultRestChannelTests.java index d633c2211b6e2..f9a43c97187bb 100644 --- a/server/src/test/java/org/elasticsearch/http/DefaultRestChannelTests.java +++ b/server/src/test/java/org/elasticsearch/http/DefaultRestChannelTests.java @@ -31,7 +31,10 @@ import org.elasticsearch.common.util.ByteArray; import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.common.util.MockPageCacheRecycler; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.rest.BytesRestResponse; @@ -148,7 +151,8 @@ public void testHeadersSet() { Settings settings = Settings.builder().build(); final TestHttpRequest httpRequest = new TestHttpRequest(HttpRequest.HttpVersion.HTTP_1_1, RestRequest.Method.GET, "/"); httpRequest.getHeaders().put(Task.X_OPAQUE_ID, Collections.singletonList("abc")); - final RestRequest request = RestRequest.request(xContentRegistry(), httpRequest, httpChannel); + final RestRequest request = RestRequest.request(xContentRegistry(), httpRequest, httpChannel, + new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); HttpHandlingSettings handlingSettings = HttpHandlingSettings.fromSettings(settings); // send a response @@ -176,7 +180,8 @@ public void testCookiesSet() { Settings settings = Settings.builder().put(HttpTransportSettings.SETTING_HTTP_RESET_COOKIES.getKey(), true).build(); final TestHttpRequest httpRequest = new TestHttpRequest(HttpRequest.HttpVersion.HTTP_1_1, RestRequest.Method.GET, "/"); httpRequest.getHeaders().put(Task.X_OPAQUE_ID, Collections.singletonList("abc")); - final RestRequest request = RestRequest.request(xContentRegistry(), httpRequest, httpChannel); + final RestRequest request = RestRequest.request(xContentRegistry(), httpRequest, httpChannel, + new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); HttpHandlingSettings handlingSettings = HttpHandlingSettings.fromSettings(settings); // send a response @@ -197,7 +202,8 @@ public void testCookiesSet() { public void testReleaseInListener() throws IOException { final Settings settings = Settings.builder().build(); final TestHttpRequest httpRequest = new TestHttpRequest(HttpRequest.HttpVersion.HTTP_1_1, RestRequest.Method.GET, "/"); - final RestRequest request = RestRequest.request(xContentRegistry(), httpRequest, httpChannel); + final RestRequest request = RestRequest.request(xContentRegistry(), httpRequest, httpChannel, + new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); HttpHandlingSettings handlingSettings = HttpHandlingSettings.fromSettings(settings); DefaultRestChannel channel = new DefaultRestChannel(httpChannel, httpRequest, request, bigArrays, handlingSettings, @@ -251,7 +257,8 @@ public void testConnectionClose() throws Exception { httpRequest.getHeaders().put(DefaultRestChannel.CONNECTION, Collections.singletonList(DefaultRestChannel.KEEP_ALIVE)); } } - final RestRequest request = RestRequest.request(xContentRegistry(), httpRequest, httpChannel); + final RestRequest request = RestRequest.request(xContentRegistry(), httpRequest, httpChannel, + new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); HttpHandlingSettings handlingSettings = HttpHandlingSettings.fromSettings(settings); @@ -283,7 +290,7 @@ public void testUnsupportedHttpMethod() { public RestRequest.Method method() { throw new IllegalArgumentException("test"); } - }, httpChannel); + }, httpChannel, new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); request.getHttpRequest().getHeaders().put(DefaultRestChannel.CONNECTION, Collections.singletonList(httpConnectionHeaderValue)); DefaultRestChannel channel = new DefaultRestChannel(httpChannel, request.getHttpRequest(), request, bigArrays, @@ -321,7 +328,7 @@ public void testCloseOnException() { public HttpResponse createResponse(RestStatus status, BytesReference content) { throw new IllegalArgumentException("test"); } - }, httpChannel); + }, httpChannel, new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); request.getHttpRequest().getHeaders().put(DefaultRestChannel.CONNECTION, Collections.singletonList(httpConnectionHeaderValue)); DefaultRestChannel channel = new DefaultRestChannel(httpChannel, request.getHttpRequest(), request, bigArrays, @@ -352,7 +359,8 @@ private TestHttpResponse executeRequest(final Settings settings, final String or httpRequest.getHeaders().put(CorsHandler.ORIGIN, Collections.singletonList(originValue)); } httpRequest.getHeaders().put(CorsHandler.HOST, Collections.singletonList(host)); - final RestRequest request = RestRequest.request(xContentRegistry(), httpRequest, httpChannel); + final RestRequest request = RestRequest.request(xContentRegistry(), httpRequest, httpChannel, + new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); HttpHandlingSettings httpHandlingSettings = HttpHandlingSettings.fromSettings(settings); RestChannel channel = new DefaultRestChannel(httpChannel, httpRequest, request, bigArrays, httpHandlingSettings, diff --git a/server/src/test/java/org/elasticsearch/node/NodeTests.java b/server/src/test/java/org/elasticsearch/node/NodeTests.java index 3741b172653a1..a8537b39e4267 100644 --- a/server/src/test/java/org/elasticsearch/node/NodeTests.java +++ b/server/src/test/java/org/elasticsearch/node/NodeTests.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.xcontent.MediaTypeParser.ParsedMediaType; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexService; import org.elasticsearch.index.engine.Engine.Searcher; @@ -345,14 +346,14 @@ public void setCircuitBreaker(CircuitBreaker circuitBreaker) { public static class TestRestCompatibility1 extends Plugin implements RestCompatibilityPlugin { @Override - public Version getCompatibleVersion(String acceptHeader, String contentTypeHeader, boolean hasContent) { + public Version getCompatibleVersion(ParsedMediaType acceptMediaType, ParsedMediaType contentType, boolean hasContent) { return Version.CURRENT.previousMajor(); } } public static class TestRestCompatibility2 extends Plugin implements RestCompatibilityPlugin { @Override - public Version getCompatibleVersion(String acceptHeader, String contentTypeHeader, boolean hasContent) { + public Version getCompatibleVersion(ParsedMediaType acceptMediaType, ParsedMediaType contentType, boolean hasContent) { return null; } } @@ -377,7 +378,7 @@ public void testCorrectUsageOfRestCompatibilityPlugin() throws IOException { try (Node node = new MockNode(settings.build(), plugins)) { CompatibleVersion restCompatibleFunction = node.getRestCompatibleFunction(); - assertThat(restCompatibleFunction.get("", "", false), equalTo(Version.CURRENT.previousMajor())); + assertThat(restCompatibleFunction.get(null, null, false), equalTo(Version.CURRENT.previousMajor())); } } @@ -390,7 +391,7 @@ public void testDefaultingRestCompatibilityPlugin() throws IOException { try (Node node = new MockNode(settings.build(), plugins)) { CompatibleVersion restCompatibleFunction = node.getRestCompatibleFunction(); - assertThat(restCompatibleFunction.get("", "", false), equalTo(Version.CURRENT)); + assertThat(restCompatibleFunction.get(null, null, false), equalTo(Version.CURRENT)); } } } diff --git a/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java b/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java index 2dd6d00b43e88..10d1026590e48 100644 --- a/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java +++ b/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java @@ -30,6 +30,8 @@ import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; @@ -98,7 +100,7 @@ public void setup() { HttpServerTransport httpServerTransport = new TestHttpServerTransport(); client = new NoOpNodeClient(this.getTestName()); restController = new RestController(Collections.emptySet(), null, client, circuitBreakerService, usageService, - , CompatibleVersion.CURRENT_VERSION); + CompatibleVersion.CURRENT_VERSION); restController.registerHandler(RestRequest.Method.GET, "/", (request, channel, client) -> channel.sendResponse( new BytesRestResponse(RestStatus.OK, BytesRestResponse.TEXT_CONTENT_TYPE, BytesArray.EMPTY))); @@ -394,7 +396,7 @@ public void testDispatchWithContentStream() { String content = randomAlphaOfLength((int) Math.round(BREAKER_LIMIT.getBytes() / inFlightRequestsBreaker.getOverhead())); final List contentTypeHeader = Collections.singletonList(mimeType); FakeRestRequest fakeRestRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY) - .withContent(new BytesArray(content), RestRequest.parseContentType(contentTypeHeader)).withPath("/foo") + .withContent(new BytesArray(content), mimeType.contains("json") ? XContentType.JSON : XContentType.SMILE).withPath("/foo") .withHeaders(Collections.singletonMap("Content-Type", contentTypeHeader)).build(); AssertingChannel channel = new AssertingChannel(fakeRestRequest, true, RestStatus.OK); restController.registerHandler(RestRequest.Method.GET, "/foo", new RestHandler() { @@ -616,7 +618,7 @@ public HttpRequest releaseAndCopy() { public Exception getInboundException() { return null; } - }, null); + }, null, new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); final AssertingChannel channel = new AssertingChannel(request, true, RestStatus.METHOD_NOT_ALLOWED); assertFalse(channel.getSendResponseCalled()); diff --git a/server/src/test/java/org/elasticsearch/rest/RestRequestTests.java b/server/src/test/java/org/elasticsearch/rest/RestRequestTests.java index 487bbed5a5999..e435e07b036a6 100644 --- a/server/src/test/java/org/elasticsearch/rest/RestRequestTests.java +++ b/server/src/test/java/org/elasticsearch/rest/RestRequestTests.java @@ -23,6 +23,8 @@ import org.elasticsearch.common.CheckedConsumer; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; @@ -94,7 +96,8 @@ private void runConsumesContentTest( when (httpRequest.getHeaders()).thenReturn( Collections.singletonMap("Content-Type", Collections.singletonList(randomFrom("application/json", "application/x-ndjson")))); final RestRequest request = - RestRequest.request(mock(NamedXContentRegistry.class), httpRequest, mock(HttpChannel.class)); + RestRequest.request(mock(NamedXContentRegistry.class), httpRequest, mock(HttpChannel.class), + new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); assertFalse(request.isContentConsumed()); try { consumer.accept(request); @@ -265,7 +268,8 @@ private static final class ContentRestRequest extends RestRequest { private ContentRestRequest(RestRequest restRequest) { super(restRequest.getXContentRegistry(), restRequest.params(), restRequest.path(), restRequest.getHeaders(), - restRequest.getHttpRequest(), restRequest.getHttpChannel()); + restRequest.getHttpRequest(), restRequest.getHttpChannel(), + new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); this.restRequest = restRequest; } diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/FakeRestRequest.java b/test/framework/src/main/java/org/elasticsearch/test/rest/FakeRestRequest.java index e36d4ae13b668..1d82e024b2d30 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/FakeRestRequest.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/FakeRestRequest.java @@ -22,6 +22,8 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.http.HttpChannel; @@ -45,7 +47,8 @@ public FakeRestRequest() { private FakeRestRequest(NamedXContentRegistry xContentRegistry, HttpRequest httpRequest, Map params, HttpChannel httpChannel) { - super(xContentRegistry, params, httpRequest.uri(), httpRequest.getHeaders(), httpRequest, httpChannel); + super(xContentRegistry, params, httpRequest.uri(), httpRequest.getHeaders(), httpRequest, httpChannel, + new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS))); } private static class FakeHttpRequest implements HttpRequest { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java index 54555c2639968..1395f2c27879c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java @@ -34,11 +34,13 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.MediaTypeParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.http.HttpServerTransport.Dispatcher; import org.elasticsearch.index.IndexModule; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.TokenizerFactory; @@ -309,16 +311,21 @@ public Map> getTransports(Settings settings, ThreadP } @Override - public Map> getHttpTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + public Map> getHttpTransports(Settings settings, + ThreadPool threadPool, + BigArrays bigArrays, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedXContentRegistry xContentRegistry, NetworkService networkService, - HttpServerTransport.Dispatcher dispatcher, - ClusterSettings clusterSettings) { + Dispatcher dispatcher, + ClusterSettings clusterSettings, + MediaTypeParser mediaTypeParser + ) { Map> transports = new HashMap<>(); filterPlugins(NetworkPlugin.class).stream().forEach(p -> transports.putAll(p.getHttpTransports(settings, threadPool, bigArrays, - pageCacheRecycler, circuitBreakerService, xContentRegistry, networkService, dispatcher, clusterSettings))); + pageCacheRecycler, circuitBreakerService, xContentRegistry, networkService, dispatcher, clusterSettings, mediaTypeParser + ))); return transports; } diff --git a/x-pack/plugin/rest-compatibility/src/main/java/org/elasticsearch/compat/CompatibleVersionPlugin.java b/x-pack/plugin/rest-compatibility/src/main/java/org/elasticsearch/compat/CompatibleVersionPlugin.java index 2d4be753839eb..107ee3af12917 100644 --- a/x-pack/plugin/rest-compatibility/src/main/java/org/elasticsearch/compat/CompatibleVersionPlugin.java +++ b/x-pack/plugin/rest-compatibility/src/main/java/org/elasticsearch/compat/CompatibleVersionPlugin.java @@ -8,68 +8,97 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.MediaTypeParser.ParsedMediaType; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.RestCompatibilityPlugin; import org.elasticsearch.rest.RestStatus; +import static org.elasticsearch.common.xcontent.XContentType.COMPATIBLE_WITH_PARAMETER_NAME; + public class CompatibleVersionPlugin extends Plugin implements RestCompatibilityPlugin { @Override - public Version getCompatibleVersion(@Nullable String acceptHeader, @Nullable String contentTypeHeader, boolean hasContent) { - Byte aVersion = XContentType.parseVersion(acceptHeader); - byte acceptVersion = aVersion == null ? Version.CURRENT.major : Integer.valueOf(aVersion).byteValue(); - Byte cVersion = XContentType.parseVersion(contentTypeHeader); - byte contentTypeVersion = cVersion == null ? Version.CURRENT.major : Integer.valueOf(cVersion).byteValue(); + public Version getCompatibleVersion(@Nullable ParsedMediaType acceptMediaType, @Nullable ParsedMediaType contentTypeMediaType, + boolean hasContent) { + final VersionInfo acceptVersionInfo = majorVersionFromMediaType(acceptMediaType); + final VersionInfo contentTypeVersionInfo = majorVersionFromMediaType(contentTypeMediaType); // accept version must be current or prior - if (acceptVersion > Version.CURRENT.major || acceptVersion < Version.CURRENT.major - 1) { + if (acceptVersionInfo.version() > Version.CURRENT.major || acceptVersionInfo.version() < Version.CURRENT.major - 1) { throw new ElasticsearchStatusException( "Compatible version must be equal or less then the current version. Accept={}} Content-Type={}}", RestStatus.BAD_REQUEST, - acceptHeader, - contentTypeHeader + acceptMediaType, // TODO implement toString!!! + contentTypeMediaType ); } if (hasContent) { // content-type version must be current or prior - if (contentTypeVersion > Version.CURRENT.major || contentTypeVersion < Version.CURRENT.major - 1) { + if (contentTypeVersionInfo.version() > Version.CURRENT.major || contentTypeVersionInfo.version() < Version.CURRENT.major - 1) { throw new ElasticsearchStatusException( "Compatible version must be equal or less then the current version. Accept={} Content-Type={}", RestStatus.BAD_REQUEST, - acceptHeader, - contentTypeHeader, + acceptMediaType, + contentTypeMediaType, RestStatus.BAD_REQUEST ); } // if both accept and content-type are sent, the version must match - if (contentTypeVersion != acceptVersion) { + if (contentTypeVersionInfo.version() != acceptVersionInfo.version()) { throw new ElasticsearchStatusException( "Content-Type and Accept version requests have to match. Accept={} Content-Type={}", RestStatus.BAD_REQUEST, - acceptHeader, - contentTypeHeader + acceptMediaType, + contentTypeMediaType ); } // both headers should be versioned or none - if ((cVersion == null && aVersion != null) || (aVersion == null && cVersion != null)) { + if (contentTypeVersionInfo.isVersioned() != acceptVersionInfo.isVersioned()) { throw new ElasticsearchStatusException( "Versioning is required on both Content-Type and Accept headers. Accept={} Content-Type={}", RestStatus.BAD_REQUEST, - acceptHeader, - contentTypeHeader + acceptMediaType, + contentTypeMediaType ); } - if (contentTypeVersion < Version.CURRENT.major) { + if (contentTypeVersionInfo.version() < Version.CURRENT.major) { return Version.CURRENT.previousMajor(); } } - if (acceptVersion < Version.CURRENT.major) { + if (acceptVersionInfo.version() < Version.CURRENT.major) { return Version.CURRENT.previousMajor(); } return Version.CURRENT; } + + private static VersionInfo majorVersionFromMediaType(@Nullable ParsedMediaType parsedMediaType) { + if (parsedMediaType != null) { + String versionString = parsedMediaType.getParameters().get(COMPATIBLE_WITH_PARAMETER_NAME); + if (versionString != null && versionString.isBlank() == false) { + return new VersionInfo(true, Byte.parseByte(versionString)); + } + } + return new VersionInfo(false, Version.CURRENT.major); + } + + private static class VersionInfo { + private final boolean isVersioned; + private final byte version; + + VersionInfo(boolean isVersioned, byte version) { + this.isVersioned = isVersioned; + this.version = version; + } + + boolean isVersioned() { + return isVersioned; + } + + byte version() { + return version; + } + } } diff --git a/x-pack/plugin/rest-compatibility/src/test/java/org/elasticsearch/compat/CompatibleVersionPluginTests.java b/x-pack/plugin/rest-compatibility/src/test/java/org/elasticsearch/compat/CompatibleVersionPluginTests.java index 32f792ffc2a92..6d9d9cc1648e3 100644 --- a/x-pack/plugin/rest-compatibility/src/test/java/org/elasticsearch/compat/CompatibleVersionPluginTests.java +++ b/x-pack/plugin/rest-compatibility/src/test/java/org/elasticsearch/compat/CompatibleVersionPluginTests.java @@ -8,11 +8,19 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.Version; +import org.elasticsearch.common.xcontent.MediaType; +import org.elasticsearch.common.xcontent.MediaTypeParser.ParsedMediaType; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.hamcrest.ElasticsearchMatchers; import org.hamcrest.Matcher; +import java.util.HashMap; +import java.util.Map; + import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; public class CompatibleVersionPluginTests extends ESTestCase { @@ -182,30 +190,86 @@ private String bodyPresent() { return "some body"; } - private String contentTypeHeader(int version) { + private ParsedMediaType contentTypeHeader(int version) { return mediaType(String.valueOf(version)); } - private String acceptHeader(int version) { + private ParsedMediaType acceptHeader(int version) { return mediaType(String.valueOf(version)); } - private String acceptHeader(String value) { - return value; + private ParsedMediaType acceptHeader(String value) { + return fromString(value); + } + + private ParsedMediaType contentTypeHeader(String value) { + return fromString(value); } - private String contentTypeHeader(String value) { - return value; + private ParsedMediaType fromString(String value) { + if (value == null) { + return null; + } + String[] splitParams = value.split(";"); + assertThat(splitParams.length, greaterThanOrEqualTo(1)); + String mediaType = splitParams[0]; + String[] splitMediaType = mediaType.split("/"); + assertThat(splitMediaType.length, is(2)); + String type = splitMediaType[0]; + String subtype = splitMediaType[1]; + + Map params; + if (splitParams.length > 1) { + params = new HashMap<>(); + for (int i = 1; i < splitParams.length; i++) { + String[] paramAndValue = splitParams[i].split("="); + assertThat(paramAndValue.length, is(2)); + params.put(paramAndValue[0], paramAndValue[1]); + } + } else { + params = Map.of(); + } + return new ParsedMediaType(new MediaType() { + @Override + public String type() { + return type; + } + + @Override + public String subtype() { + return subtype; + } + + @Override + public String format() { + return null; + } + }, params); } - private String mediaType(String version) { + private ParsedMediaType mediaType(String version) { if (version != null) { - return "application/vnd.elasticsearch+json;compatible-with=" + version; + return new ParsedMediaType(new MediaType() { + @Override + public String type() { + return "application"; + } + + @Override + public String subtype() { + return "vnd.elasticsearch+json"; + } + + @Override + public String format() { + return null; + } + }, Map.of(XContentType.COMPATIBLE_WITH_PARAMETER_NAME, version)); } return null; } - private Version requestWith(String accept, String contentType, String body) { + private Version requestWith(ParsedMediaType accept, ParsedMediaType contentType, String body) { return compatibleVersionPlugin.getCompatibleVersion(accept, contentType, body.isEmpty() == false); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 4474e401ecbaf..97e62685463d2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -37,10 +37,12 @@ import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.MediaTypeParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.http.HttpServerTransport.Dispatcher; import org.elasticsearch.index.IndexModule; import org.elasticsearch.indices.SystemIndexDescriptor; import org.elasticsearch.indices.breaker.CircuitBreakerService; @@ -988,13 +990,17 @@ public Map> getTransports(Settings settings, ThreadP } @Override - public Map> getHttpTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, + public Map> getHttpTransports(Settings settings, + ThreadPool threadPool, + BigArrays bigArrays, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedXContentRegistry xContentRegistry, NetworkService networkService, - HttpServerTransport.Dispatcher dispatcher, - ClusterSettings clusterSettings) { + Dispatcher dispatcher, + ClusterSettings clusterSettings, + MediaTypeParser mediaTypeParser + ) { if (enabled == false) { // don't register anything if we are not enabled return Collections.emptyMap(); } @@ -1002,10 +1008,10 @@ public Map> getHttpTransports(Settings set Map> httpTransports = new HashMap<>(); httpTransports.put(SecurityField.NAME4, () -> new SecurityNetty4HttpServerTransport(settings, networkService, bigArrays, ipFilter.get(), getSslService(), threadPool, xContentRegistry, dispatcher, clusterSettings, - getNettySharedGroupFactory(settings))); + getNettySharedGroupFactory(settings), mediaTypeParser)); httpTransports.put(SecurityField.NIO, () -> new SecurityNioHttpServerTransport(settings, networkService, bigArrays, pageCacheRecycler, threadPool, xContentRegistry, dispatcher, ipFilter.get(), getSslService(), getNioGroupFactory(settings), - clusterSettings)); + clusterSettings, mediaTypeParser)); return httpTransports; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java index e3207605658f0..03e9a3f9fd0d0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransport.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.xcontent.MediaTypeParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.http.HttpChannel; import org.elasticsearch.http.netty4.Netty4HttpServerTransport; @@ -39,8 +40,9 @@ public class SecurityNetty4HttpServerTransport extends Netty4HttpServerTransport public SecurityNetty4HttpServerTransport(Settings settings, NetworkService networkService, BigArrays bigArrays, IPFilter ipFilter, SSLService sslService, ThreadPool threadPool, NamedXContentRegistry xContentRegistry, Dispatcher dispatcher, ClusterSettings clusterSettings, - SharedGroupFactory sharedGroupFactory) { - super(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher, clusterSettings, sharedGroupFactory); + SharedGroupFactory sharedGroupFactory, MediaTypeParser mediaTypeParser) { + super(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher, clusterSettings, sharedGroupFactory, + mediaTypeParser); this.securityExceptionHandler = new SecurityHttpExceptionHandler(logger, lifecycle, (c, e) -> super.onException(c, e)); this.ipFilter = ipFilter; final boolean ssl = HTTP_SSL_ENABLED.get(settings); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransport.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransport.java index ff2c91da208ea..02e78054c0f39 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransport.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransport.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.xcontent.MediaTypeParser; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.http.nio.HttpReadWriteHandler; import org.elasticsearch.http.nio.NioHttpChannel; @@ -55,9 +56,9 @@ public SecurityNioHttpServerTransport(Settings settings, NetworkService networkS PageCacheRecycler pageCacheRecycler, ThreadPool threadPool, NamedXContentRegistry xContentRegistry, Dispatcher dispatcher, IPFilter ipFilter, SSLService sslService, NioGroupFactory nioGroupFactory, - ClusterSettings clusterSettings) { + ClusterSettings clusterSettings, MediaTypeParser mediaTypeParser) { super(settings, networkService, bigArrays, pageCacheRecycler, threadPool, xContentRegistry, dispatcher, nioGroupFactory, - clusterSettings); + clusterSettings, mediaTypeParser); this.securityExceptionHandler = new SecurityHttpExceptionHandler(logger, lifecycle, (c, e) -> super.onException(c, e)); this.ipFilter = ipFilter; this.sslEnabled = HTTP_SSL_ENABLED.get(settings); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java index 951c2d5fb4238..0e4d144e6cebd 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/netty4/SecurityNetty4HttpServerTransportTests.java @@ -13,6 +13,9 @@ import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.http.NullDispatcher; @@ -42,6 +45,8 @@ public class SecurityNetty4HttpServerTransportTests extends ESTestCase { private Environment env; private Path testnodeCert; private Path testnodeKey; + private MediaTypeParser mediaTypeParser; + @Before public void createSSLService() { testnodeCert = getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"); @@ -58,6 +63,7 @@ public void createSSLService() { .build(); env = TestEnvironment.newEnvironment(settings); sslService = new SSLService(env); + mediaTypeParser = new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS)); } public void testDefaultClientAuth() throws Exception { @@ -68,7 +74,8 @@ public void testDefaultClientAuth() throws Exception { SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings), + mediaTypeParser); ChannelHandler handler = transport.configureServerChannelHandler(); final EmbeddedChannel ch = new EmbeddedChannel(handler); assertThat(ch.pipeline().get(SslHandler.class).engine().getNeedClientAuth(), is(false)); @@ -85,7 +92,8 @@ public void testOptionalClientAuth() throws Exception { SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings), + mediaTypeParser); ChannelHandler handler = transport.configureServerChannelHandler(); final EmbeddedChannel ch = new EmbeddedChannel(handler); assertThat(ch.pipeline().get(SslHandler.class).engine().getNeedClientAuth(), is(false)); @@ -102,7 +110,8 @@ public void testRequiredClientAuth() throws Exception { SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings), + mediaTypeParser); ChannelHandler handler = transport.configureServerChannelHandler(); final EmbeddedChannel ch = new EmbeddedChannel(handler); assertThat(ch.pipeline().get(SslHandler.class).engine().getNeedClientAuth(), is(true)); @@ -119,7 +128,8 @@ public void testNoClientAuth() throws Exception { SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings), + mediaTypeParser); ChannelHandler handler = transport.configureServerChannelHandler(); final EmbeddedChannel ch = new EmbeddedChannel(handler); assertThat(ch.pipeline().get(SslHandler.class).engine().getNeedClientAuth(), is(false)); @@ -134,7 +144,8 @@ public void testCustomSSLConfiguration() throws Exception { SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings), + mediaTypeParser); ChannelHandler handler = transport.configureServerChannelHandler(); EmbeddedChannel ch = new EmbeddedChannel(handler); SSLEngine defaultEngine = ch.pipeline().get(SslHandler.class).engine(); @@ -147,7 +158,8 @@ public void testCustomSSLConfiguration() throws Exception { sslService = new SSLService(TestEnvironment.newEnvironment(settings)); transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings), + mediaTypeParser); handler = transport.configureServerChannelHandler(); ch = new EmbeddedChannel(handler); SSLEngine customEngine = ch.pipeline().get(SslHandler.class).engine(); @@ -170,7 +182,8 @@ public void testNoExceptionWhenConfiguredWithoutSslKeySSLDisabled() throws Excep SecurityNetty4HttpServerTransport transport = new SecurityNetty4HttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(IPFilter.class), sslService, mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), new SharedGroupFactory(settings), + mediaTypeParser); assertNotNull(transport.configureServerChannelHandler()); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransportTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransportTests.java index 39d19578cec6f..9b028e1155e05 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransportTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SecurityNioHttpServerTransportTests.java @@ -11,6 +11,9 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.http.NullDispatcher; @@ -49,6 +52,7 @@ public class SecurityNioHttpServerTransportTests extends ESTestCase { private Environment env; private InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); private NioGroupFactory nioGroupFactory; + private MediaTypeParser mediaTypeParser; @Before public void createSSLService() { @@ -65,6 +69,7 @@ public void createSSLService() { .build(); env = TestEnvironment.newEnvironment(settings); sslService = new SSLService(env); + mediaTypeParser = new MediaTypeParser<>(new MediaTypeRegistry(XContentType.MEDIA_TYPE_DEFINITIONS)); } public void testDefaultClientAuth() throws IOException { @@ -76,7 +81,7 @@ public void testDefaultClientAuth() throws IOException { SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService, nioGroupFactory, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser); SecurityNioHttpServerTransport.SecurityHttpChannelFactory factory = transport.channelFactory(); SocketChannel socketChannel = mock(SocketChannel.class); when(socketChannel.getRemoteAddress()).thenReturn(address); @@ -98,7 +103,7 @@ public void testOptionalClientAuth() throws IOException { SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService, nioGroupFactory, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser); SecurityNioHttpServerTransport.SecurityHttpChannelFactory factory = transport.channelFactory(); SocketChannel socketChannel = mock(SocketChannel.class); @@ -120,7 +125,7 @@ public void testRequiredClientAuth() throws IOException { SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService, nioGroupFactory, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser); SecurityNioHttpServerTransport.SecurityHttpChannelFactory factory = transport.channelFactory(); SocketChannel socketChannel = mock(SocketChannel.class); @@ -142,7 +147,7 @@ public void testNoClientAuth() throws IOException { SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService, nioGroupFactory, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser); SecurityNioHttpServerTransport.SecurityHttpChannelFactory factory = transport.channelFactory(); SocketChannel socketChannel = mock(SocketChannel.class); @@ -162,7 +167,7 @@ public void testCustomSSLConfiguration() throws IOException { SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService, nioGroupFactory, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser); SecurityNioHttpServerTransport.SecurityHttpChannelFactory factory = transport.channelFactory(); SocketChannel socketChannel = mock(SocketChannel.class); when(socketChannel.getRemoteAddress()).thenReturn(address); @@ -179,7 +184,7 @@ public void testCustomSSLConfiguration() throws IOException { transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService, nioGroupFactory, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser); factory = transport.channelFactory(); channel = factory.createChannel(mock(NioSelector.class), socketChannel, mock(Config.Socket.class)); SSLEngine customEngine = SSLEngineUtils.getSSLEngine(channel); @@ -203,6 +208,6 @@ public void testNoExceptionWhenConfiguredWithoutSslKeySSLDisabled() { SecurityNioHttpServerTransport transport = new SecurityNioHttpServerTransport(settings, new NetworkService(Collections.emptyList()), mock(BigArrays.class), mock(PageCacheRecycler.class), mock(ThreadPool.class), xContentRegistry(), new NullDispatcher(), mock(IPFilter.class), sslService, nioGroupFactory, - new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)); + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), mediaTypeParser); } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/RestSqlQueryAction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/RestSqlQueryAction.java index 763e460170cf0..5cb3df91663be 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/RestSqlQueryAction.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/RestSqlQueryAction.java @@ -8,6 +8,7 @@ import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.xcontent.MediaType; +import org.elasticsearch.common.xcontent.MediaTypeParser.ParsedMediaType; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; @@ -20,6 +21,7 @@ import org.elasticsearch.xpack.sql.action.SqlQueryAction; import org.elasticsearch.xpack.sql.action.SqlQueryRequest; import org.elasticsearch.xpack.sql.action.SqlQueryResponse; +import org.elasticsearch.xpack.sql.proto.Mode; import org.elasticsearch.xpack.sql.proto.Protocol; import java.io.IOException; @@ -31,10 +33,10 @@ import static org.elasticsearch.rest.RestRequest.Method.GET; import static org.elasticsearch.rest.RestRequest.Method.POST; import static org.elasticsearch.xpack.sql.proto.Protocol.URL_PARAM_DELIMITER; +import static org.elasticsearch.xpack.sql.proto.Protocol.URL_PARAM_FORMAT; public class RestSqlQueryAction extends BaseRestHandler { - private final SqlMediaTypeParser sqlMediaTypeParser = new SqlMediaTypeParser(); MediaType responseMediaType; @Override @@ -52,7 +54,9 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli sqlRequest = SqlQueryRequest.fromXContent(parser); } - responseMediaType = sqlMediaTypeParser.getMediaType(request, sqlRequest); + final MediaType localMediaType = getMediaType(request, sqlRequest); + // this is a hack and we shouldn't rely on the class instance value asynchronously + this.responseMediaType = localMediaType; long startNanos = System.nanoTime(); return channel -> client.execute(SqlQueryAction.INSTANCE, sqlRequest, new RestResponseListener(channel) { @@ -61,13 +65,13 @@ public RestResponse buildResponse(SqlQueryResponse response) throws Exception { RestResponse restResponse; // XContent branch - if (responseMediaType != null && responseMediaType instanceof XContentType) { - XContentType type = (XContentType) responseMediaType; + if (localMediaType instanceof XContentType) { + XContentType type = (XContentType) localMediaType; XContentBuilder builder = channel.newBuilder(request.getXContentType(), type, true); response.toXContent(builder, request); restResponse = new BytesRestResponse(RestStatus.OK, builder); } else { // TextFormat - TextFormat type = (TextFormat)responseMediaType; + TextFormat type = (TextFormat) localMediaType; final String data = type.format(request, response); restResponse = new BytesRestResponse(RestStatus.OK, type.contentType(request), @@ -84,14 +88,48 @@ public RestResponse buildResponse(SqlQueryResponse response) throws Exception { }); } - - - @Override protected Set responseParams() { return responseMediaType == TextFormat.CSV ? Collections.singleton(URL_PARAM_DELIMITER) : Collections.emptySet(); } + /* + * Since we support {@link TextFormat} and + * {@link XContent} outputs we can't use {@link RestToXContentListener} + * like everything else. We want to stick as closely as possible to + * Elasticsearch's defaults though, while still layering in ways to + * control the output more easily. + * + * First we find the string that the user used to specify the response + * format. If there is a {@code format} parameter we use that. If there + * isn't but there is a {@code Accept} header then we use that. If there + * isn't then we use the {@code Content-Type} header which is required. + */ + public MediaType getMediaType(RestRequest request, SqlQueryRequest sqlRequest) { + if (Mode.isDedicatedClient(sqlRequest.requestInfo().mode()) + && (sqlRequest.binaryCommunication() == null || sqlRequest.binaryCommunication())) { + // enforce CBOR response for drivers and CLI (unless instructed differently through the config param) + return XContentType.CBOR; + } else if (request.getFormatMediaType() != null) { + return validateColumnarRequest(sqlRequest.columnar(), request.getFormatMediaType()); + } + if (request.getAcceptMediaType() != null) { + return validateColumnarRequest(sqlRequest.columnar(), request.getAcceptMediaType().getMediaType()); + } + + ParsedMediaType contentType = request.getContentType(); + assert contentType != null : "The Content-Type header is required"; + return validateColumnarRequest(sqlRequest.columnar(), contentType.getMediaType()); + } + + private static MediaType validateColumnarRequest(boolean requestIsColumnar, MediaType fromMediaType) { + if (requestIsColumnar && fromMediaType instanceof TextFormat){ + throw new IllegalArgumentException("Invalid use of [columnar] argument: cannot be used in combination with " + + "txt, csv or tsv formats"); + } + return fromMediaType; + } + @Override public String getName() { return "sql_query"; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/SqlMediaTypeParser.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/SqlMediaTypeParser.java deleted file mode 100644 index 189dc137b654c..0000000000000 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/SqlMediaTypeParser.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.sql.plugin; - -import org.elasticsearch.common.xcontent.MediaType; -import org.elasticsearch.common.xcontent.MediaTypeParser; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.xpack.sql.action.SqlQueryRequest; -import org.elasticsearch.xpack.sql.proto.Mode; - -import java.util.Map; - -import static org.elasticsearch.xpack.sql.proto.Protocol.URL_PARAM_FORMAT; - -public class SqlMediaTypeParser { - private static final MediaTypeParser parser = new MediaTypeParser.Builder<>() - .copyFromMediaTypeParser(XContentType.mediaTypeParser) - .withMediaTypeAndParams(TextFormat.PLAIN_TEXT.typeWithSubtype(), TextFormat.PLAIN_TEXT, - Map.of("header", "present|absent", "charset", "utf-8")) - .withMediaTypeAndParams(TextFormat.CSV.typeWithSubtype(), TextFormat.CSV, - Map.of("header", "present|absent", "charset", "utf-8", - "delimiter", ".+"))// more detailed parsing is in TextFormat.CSV#delimiter - .withMediaTypeAndParams(TextFormat.TSV.typeWithSubtype(), TextFormat.TSV, - Map.of("header", "present|absent", "charset", "utf-8")) - .build(); - - /* - * Since we support {@link TextFormat} and - * {@link XContent} outputs we can't use {@link RestToXContentListener} - * like everything else. We want to stick as closely as possible to - * Elasticsearch's defaults though, while still layering in ways to - * control the output more easily. - * - * First we find the string that the user used to specify the response - * format. If there is a {@code format} parameter we use that. If there - * isn't but there is a {@code Accept} header then we use that. If there - * isn't then we use the {@code Content-Type} header which is required. - */ - public MediaType getMediaType(RestRequest request, SqlQueryRequest sqlRequest) { - - if (Mode.isDedicatedClient(sqlRequest.requestInfo().mode()) - && (sqlRequest.binaryCommunication() == null || sqlRequest.binaryCommunication())) { - // enforce CBOR response for drivers and CLI (unless instructed differently through the config param) - return XContentType.CBOR; - } else if (request.hasParam(URL_PARAM_FORMAT)) { - return validateColumnarRequest(sqlRequest.columnar(), parser.fromFormat(request.param(URL_PARAM_FORMAT))); - } - if (request.getHeaders().containsKey("Accept")) { - String accept = request.header("Accept"); - // */* means "I don't care" which we should treat like not specifying the header - if ("*/*".equals(accept) == false) { - return validateColumnarRequest(sqlRequest.columnar(), parser.fromMediaType(accept)); - } - } - - String contentType = request.header("Content-Type"); - assert contentType != null : "The Content-Type header is required"; - return validateColumnarRequest(sqlRequest.columnar(), parser.fromMediaType(contentType)); - } - - private static MediaType validateColumnarRequest(boolean requestIsColumnar, MediaType fromMediaType) { - if(requestIsColumnar && fromMediaType instanceof TextFormat){ - throw new IllegalArgumentException("Invalid use of [columnar] argument: cannot be used in combination with " - + "txt, csv or tsv formats"); - } - return fromMediaType; - } - -} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/SqlPlugin.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/SqlPlugin.java index 6f490f892b712..ec9a6396f5c4a 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/SqlPlugin.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/SqlPlugin.java @@ -16,7 +16,10 @@ import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.common.xcontent.MediaTypeDefinition; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.license.LicenseUtils; @@ -45,8 +48,12 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.function.Supplier; +import static org.elasticsearch.common.xcontent.XContentType.COMPATIBLE_WITH_PARAMETER_NAME; +import static org.elasticsearch.common.xcontent.XContentType.VERSION_PATTERN; + public class SqlPlugin extends Plugin implements ActionPlugin { private final SqlLicenseChecker sqlLicenseChecker = new SqlLicenseChecker( @@ -124,4 +131,20 @@ public List getRestHandlers(Settings settings, RestController restC usageAction, infoAction); } + + @Override + public List getAdditionalMediaTypes() { + return List.of( + new MediaTypeDefinition(TextFormat.PLAIN_TEXT, Map.of("header", "present|absent", "charset", "utf-8")), + new MediaTypeDefinition(TextFormat.CSV, Map.of("header", "present|absent", "charset", "utf-8", "delimiter", ".+")), + new MediaTypeDefinition(TextFormat.TSV, Map.of("header", "present|absent", "charset", "utf-8")), + new MediaTypeDefinition("text/vnd.elasticsearch+plain", TextFormat.PLAIN_TEXT, + Map.of("header", "present|absent", "charset", "utf-8", COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN)), + new MediaTypeDefinition("text/vnd.elasticsearch+csv", TextFormat.CSV, + Map.of("header", "present|absent", "charset", "utf-8", "delimiter", ".+", COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN)), + new MediaTypeDefinition("text/vnd.elasticsearch+tsv", TextFormat.TSV, + Map.of("header", "present|absent", "charset", "utf-8", COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN)) + ); + } + } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/TextFormat.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/TextFormat.java index d397d2316959c..71fd4babc5701 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/TextFormat.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/TextFormat.java @@ -24,8 +24,10 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.function.Function; +import java.util.regex.Pattern; import static org.elasticsearch.xpack.sql.action.BasicFormatter.FormatOption.TEXT; import static org.elasticsearch.xpack.sql.proto.Protocol.URL_PARAM_DELIMITER; @@ -107,7 +109,11 @@ public String subtype() { return "plain"; } - + @Override + public Map validatedParameters() { + return Map.of("header", Pattern.compile("present|absent", Pattern.CASE_INSENSITIVE), + "charset", Pattern.compile("utf-8", Pattern.CASE_INSENSITIVE)); + } }, /** @@ -227,6 +233,13 @@ boolean hasHeader(RestRequest request) { public String subtype() { return "csv"; } + + @Override + public Map validatedParameters() { + return Map.of("header", Pattern.compile("present|absent", Pattern.CASE_INSENSITIVE), + "charset", Pattern.compile("utf-8", Pattern.CASE_INSENSITIVE), + "delimiter", Pattern.compile(".+", Pattern.CASE_INSENSITIVE)); + } }, TSV() { @@ -281,6 +294,12 @@ String maybeEscape(String value, Character __) { public String subtype() { return "tab-separated-values"; } + + @Override + public Map validatedParameters() { + return Map.of("header", Pattern.compile("present|absent", Pattern.CASE_INSENSITIVE), + "charset", Pattern.compile("utf-8", Pattern.CASE_INSENSITIVE)); + } }; private static final String FORMAT_TEXT = "txt"; diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plugin/SqlMediaTypeParserTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plugin/SqlMediaTypeParserTests.java deleted file mode 100644 index 0459b777b15f9..0000000000000 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/plugin/SqlMediaTypeParserTests.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.sql.plugin; - -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.MediaType; -import org.elasticsearch.common.xcontent.NamedXContentRegistry; -import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.rest.FakeRestRequest; -import org.elasticsearch.xpack.sql.action.SqlQueryRequest; -import org.elasticsearch.xpack.sql.proto.Mode; -import org.elasticsearch.xpack.sql.proto.RequestInfo; - -import java.util.Collections; -import java.util.Map; - -import static org.elasticsearch.xpack.sql.plugin.TextFormat.CSV; -import static org.elasticsearch.xpack.sql.plugin.TextFormat.PLAIN_TEXT; -import static org.elasticsearch.xpack.sql.plugin.TextFormat.TSV; -import static org.elasticsearch.xpack.sql.proto.RequestInfo.CLIENT_IDS; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; - -public class SqlMediaTypeParserTests extends ESTestCase { - SqlMediaTypeParser parser = new SqlMediaTypeParser(); - - public void testPlainTextDetection() { - MediaType text = parser.getMediaType(reqWithAccept("text/plain"), createTestInstance(false, Mode.PLAIN, false)); - assertThat(text, is(PLAIN_TEXT)); - } - - public void testCsvDetection() { - MediaType text = parser.getMediaType(reqWithAccept("text/csv"), createTestInstance(false, Mode.PLAIN, false)); - assertThat(text, is(CSV)); - - text = parser.getMediaType(reqWithAccept("text/csv; delimiter=x"), createTestInstance(false, Mode.PLAIN, false)); - assertThat(text, is(CSV)); - } - - public void testTsvDetection() { - MediaType text = parser.getMediaType(reqWithAccept("text/tab-separated-values"), createTestInstance(false, Mode.PLAIN, false)); - assertThat(text, is(TSV)); - } - - public void testMediaTypeDetectionWithParameters() { - assertThat(parser.getMediaType(reqWithAccept("text/plain; charset=utf-8"), - createTestInstance(false, Mode.PLAIN, false)), is(PLAIN_TEXT)); - assertThat(parser.getMediaType(reqWithAccept("text/plain; header=present"), - createTestInstance(false, Mode.PLAIN, false)), is(PLAIN_TEXT)); - assertThat(parser.getMediaType(reqWithAccept("text/plain; charset=utf-8; header=present"), - createTestInstance(false, Mode.PLAIN, false)), is(PLAIN_TEXT)); - - assertThat(parser.getMediaType(reqWithAccept("text/csv; charset=utf-8"), - createTestInstance(false, Mode.PLAIN, false)), is(CSV)); - assertThat(parser.getMediaType(reqWithAccept("text/csv; header=present"), - createTestInstance(false, Mode.PLAIN, false)), is(CSV)); - assertThat(parser.getMediaType(reqWithAccept("text/csv; charset=utf-8; header=present"), - createTestInstance(false, Mode.PLAIN, false)), is(CSV)); - - assertThat(parser.getMediaType(reqWithAccept("text/tab-separated-values; charset=utf-8"), - createTestInstance(false, Mode.PLAIN, false)), is(TSV)); - assertThat(parser.getMediaType(reqWithAccept("text/tab-separated-values; header=present"), - createTestInstance(false, Mode.PLAIN, false)), is(TSV)); - assertThat(parser.getMediaType(reqWithAccept("text/tab-separated-values; charset=utf-8; header=present"), - createTestInstance(false, Mode.PLAIN, false)), is(TSV)); - } - - public void testInvalidFormat() { - MediaType mediaType = parser.getMediaType(reqWithAccept("text/garbage"), createTestInstance(false, Mode.PLAIN, false)); - assertThat(mediaType, is(nullValue())); - } - - private static RestRequest reqWithAccept(String acceptHeader) { - - return new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY) - .withHeaders(Map.of("Content-Type", Collections.singletonList("application/json"), - "Accept", Collections.singletonList(acceptHeader))) - .build(); - } - - protected SqlQueryRequest createTestInstance(boolean binaryCommunication, Mode mode, boolean columnar) { - return new SqlQueryRequest(randomAlphaOfLength(10), Collections.emptyList(), null, - randomZone(), between(1, Integer.MAX_VALUE), TimeValue.parseTimeValue(randomTimeValue(), null, "test"), - TimeValue.parseTimeValue(randomTimeValue(), null, "test"), columnar, randomAlphaOfLength(10), - new RequestInfo(mode, randomFrom(randomFrom(CLIENT_IDS), randomAlphaOfLengthBetween(10, 20))), - randomBoolean(), randomBoolean()).binaryCommunication(binaryCommunication); - } -}