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/MediaTypeParser.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeParser.java index 62a3f3fd915d0..feee9d10bce7f 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,22 @@ 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 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 +61,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 +86,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 +100,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 { 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 +134,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..913d4d677b274 --- /dev/null +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeRegistry.java @@ -0,0 +1,97 @@ +/* + * 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.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +public class MediaTypeRegistry { + + private Map formatToMediaType = new ConcurrentHashMap<>(); + private Map typeWithSubtypeToMediaType = new ConcurrentHashMap<>(); + private Map> parametersMap = new ConcurrentHashMap<>(); + + public MediaTypeRegistry register(Map formatToMediaType, Map typeWithSubtypeToMediaType, Map> parametersMap) { + this.formatToMediaType.putAll(formatToMediaType); + this.typeWithSubtypeToMediaType.putAll(typeWithSubtypeToMediaType); + this.parametersMap.putAll(parametersMap); + return this; + } + + public MediaTypeRegistry register(String typeWithSubtype, T mediaType, String format, Map parametersMap) { + if (format != null) { + this.formatToMediaType.put(format, mediaType); + } + this.typeWithSubtypeToMediaType.put(typeWithSubtype,mediaType); + Map parametersForMediaType = new HashMap<>(parametersMap.size()); + for (Map.Entry params : parametersMap.entrySet()) { + String parameterName = params.getKey().toLowerCase(Locale.ROOT); + String parameterRegex = params.getValue(); + Pattern pattern = Pattern.compile(parameterRegex, Pattern.CASE_INSENSITIVE); + parametersForMediaType.put(parameterName, pattern); + } + this.parametersMap.put(typeWithSubtype,parametersForMediaType); + return this; + } + 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); + } + + public MediaTypeRegistry register(String alternativeMediaType, MediaType 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 MediaTypeRegistry register(MediaTypeRegistry xContentTypeRegistry) { + formatToMediaType.putAll(xContentTypeRegistry.formatToMediaType); + typeWithSubtypeToMediaType.putAll(xContentTypeRegistry.typeWithSubtypeToMediaType); + parametersMap.putAll(xContentTypeRegistry.parametersMap); + return this; + } + public MediaTypeRegistry register(Collection mediaTypeRegistries ) { + for (MediaTypeRegistry mediaTypeRegistry : mediaTypeRegistries) { + register(mediaTypeRegistry); + } + return this; + } + +} 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..be8d9189f6cd4 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 @@ -114,26 +114,32 @@ 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+"; + + private static final MediaTypeRegistry mediaTypeRegistry = new MediaTypeRegistry() + .register("application/smile", SMILE, Collections.emptyMap()) + .register("application/cbor", CBOR, Collections.emptyMap()) + .register("application/json", JSON, Map.of("charset", "UTF-8")) + .register("application/yaml", YAML, Map.of("charset", "UTF-8")) + .register("application/*", JSON, Map.of("charset", "UTF-8")) + .register("application/x-ndjson", JSON, Map.of("charset", "UTF-8")) + .register("application/vnd.elasticsearch+json", JSON, Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")) - .withMediaTypeAndParams("application/vnd.elasticsearch+smile", SMILE, + .register("application/vnd.elasticsearch+smile", SMILE, Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")) - .withMediaTypeAndParams("application/vnd.elasticsearch+yaml", YAML, + .register("application/vnd.elasticsearch+yaml", YAML, Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")) - .withMediaTypeAndParams("application/vnd.elasticsearch+cbor", CBOR, + .register("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(); + .register("application/vnd.elasticsearch+x-ndjson", JSON, + Map.of(COMPATIBLE_WITH_PARAMETER_NAME, VERSION_PATTERN, "charset", "UTF-8")); + + private static MediaTypeParser mediaTypeParser = new MediaTypeParser<>(mediaTypeRegistry); + + public static MediaTypeRegistry getMediaTypeRegistry() { + return mediaTypeRegistry; + } /** * Accepts a format string, which is most of the time is equivalent to {@link XContentType#subtype()} 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..b9137adf9c2ea 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,12 @@ import static org.hamcrest.Matchers.nullValue; public class MediaTypeParserTests extends ESTestCase { - - MediaTypeParser mediaTypeParser = new MediaTypeParser.Builder() + MediaTypeRegistry mediaTypeRegistry = new MediaTypeRegistry(); + MediaTypeParser mediaTypeParser = new MediaTypeParser.Builder() .withMediaTypeAndParams("application/vnd.elasticsearch+json", XContentType.JSON, Map.of("compatible-with", "\\d+", "charset", "UTF-8")) - .build(); + .build(mediaTypeRegistry); public void testJsonWithParameters() throws Exception { String mediaType = "application/vnd.elasticsearch+json"; @@ -74,4 +74,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/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 15ca8e4f66bbd..4cf13f117d62c 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -87,7 +87,9 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; +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 +332,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,10 +532,25 @@ protected Node(final Environment initialEnvironment, repositoriesServiceReference::get).stream()) .collect(Collectors.toList()); + Collection mediaTypesFromPlugins = pluginsService.filterPlugins(ActionPlugin.class) + .stream() + .map(ActionPlugin::getAdditionalMediaTypes) + .collect(toList()); + + MediaTypeRegistry globalMediaTypeRegistry = new MediaTypeRegistry() + .register(mediaTypesFromPlugins) + .register(XContentType.getMediaTypeRegistry()); + + // passes down to SQL and CompatibleVersion plugins +// pluginsService.filterPlugins(MediaTypeRegistryPlugin.class) +// .forEach(plugin -> plugin.setGlobalMediaTypeRegistry(globalMediaTypeRegistry)); + + CompatibleVersion restCompatibleFunction = getRestCompatibleFunction(globalMediaTypeRegistry); + 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(); @@ -711,13 +729,14 @@ protected Node(final Environment initialEnvironment, * @return A function that can be used to determine the requested REST compatible version * package scope for testing */ - CompatibleVersion getRestCompatibleFunction() { + CompatibleVersion getRestCompatibleFunction(MediaTypeRegistry globalMediaTypeRegistry) { List restCompatibilityPlugins = pluginsService.filterPlugins(RestCompatibilityPlugin.class); final CompatibleVersion compatibleVersion; if (restCompatibilityPlugins.size() > 1) { throw new IllegalStateException("Only one RestCompatibilityPlugin is allowed"); } else if (restCompatibilityPlugins.size() == 1) { - compatibleVersion = restCompatibilityPlugins.get(0)::getCompatibleVersion; + compatibleVersion = (acceptHeader, contentTypeHeader, hasContent) -> + restCompatibilityPlugins.get(0).getCompatibleVersion(acceptHeader, contentTypeHeader, hasContent, globalMediaTypeRegistry); } else { compatibleVersion = CompatibleVersion.CURRENT_VERSION; } diff --git a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java index ef9861f266222..266a0e7550be7 100644 --- a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java @@ -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.MediaTypeRegistry; 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 MediaTypeRegistry getAdditionalMediaTypes(){ + return new MediaTypeRegistry(); + } } diff --git a/server/src/main/java/org/elasticsearch/plugins/RestCompatibilityPlugin.java b/server/src/main/java/org/elasticsearch/plugins/RestCompatibilityPlugin.java index 9fd73a24ee87b..cea394036d6e2 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.MediaTypeRegistry; /** @@ -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 String acceptHeader, @Nullable String contentTypeHeader, boolean hasContent, MediaTypeRegistry mediaTypeRegistry); + } diff --git a/server/src/test/java/org/elasticsearch/node/NodeTests.java b/server/src/test/java/org/elasticsearch/node/NodeTests.java index 3741b172653a1..e6e60c6d5bdd4 100644 --- a/server/src/test/java/org/elasticsearch/node/NodeTests.java +++ b/server/src/test/java/org/elasticsearch/node/NodeTests.java @@ -28,6 +28,8 @@ import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexService; import org.elasticsearch.index.engine.Engine.Searcher; @@ -345,14 +347,16 @@ 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(String acceptHeader, String contentTypeHeader, boolean hasContent, + MediaTypeRegistry mediaTypeRegistry) { 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(String acceptHeader, String contentTypeHeader, boolean hasContent, + MediaTypeRegistry mediaTypeRegistry) { return null; } } @@ -376,7 +380,7 @@ public void testCorrectUsageOfRestCompatibilityPlugin() throws IOException { plugins.add(TestRestCompatibility1.class); try (Node node = new MockNode(settings.build(), plugins)) { - CompatibleVersion restCompatibleFunction = node.getRestCompatibleFunction(); + CompatibleVersion restCompatibleFunction = node.getRestCompatibleFunction(XContentType.getMediaTypeRegistry()); assertThat(restCompatibleFunction.get("", "", false), equalTo(Version.CURRENT.previousMajor())); } } @@ -389,7 +393,7 @@ public void testDefaultingRestCompatibilityPlugin() throws IOException { List> plugins = basePlugins(); try (Node node = new MockNode(settings.build(), plugins)) { - CompatibleVersion restCompatibleFunction = node.getRestCompatibleFunction(); + CompatibleVersion restCompatibleFunction = node.getRestCompatibleFunction(XContentType.getMediaTypeRegistry()); assertThat(restCompatibleFunction.get("", "", 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..4346600692091 100644 --- a/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java +++ b/server/src/test/java/org/elasticsearch/rest/RestControllerTests.java @@ -98,7 +98,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))); 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..fb8942155d6dc 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,7 +8,9 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.MediaType; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.RestCompatibilityPlugin; import org.elasticsearch.rest.RestStatus; @@ -16,10 +18,12 @@ public class CompatibleVersionPlugin extends Plugin implements RestCompatibilityPlugin { @Override - public Version getCompatibleVersion(@Nullable String acceptHeader, @Nullable String contentTypeHeader, boolean hasContent) { - Byte aVersion = XContentType.parseVersion(acceptHeader); + public Version getCompatibleVersion(@Nullable String acceptHeader, @Nullable String contentTypeHeader, boolean hasContent, + MediaTypeRegistry mediaTypeRegistry) { + MediaTypeParser mediaTypeParser = new MediaTypeParser<>(mediaTypeRegistry); + Byte aVersion = mediaTypeParser.parseVersion(acceptHeader); byte acceptVersion = aVersion == null ? Version.CURRENT.major : Integer.valueOf(aVersion).byteValue(); - Byte cVersion = XContentType.parseVersion(contentTypeHeader); + Byte cVersion = mediaTypeParser.parseVersion(contentTypeHeader); byte contentTypeVersion = cVersion == null ? Version.CURRENT.major : Integer.valueOf(cVersion).byteValue(); // accept version must be current or prior 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..334a13dd62647 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,6 +8,7 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.Version; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.hamcrest.ElasticsearchMatchers; import org.hamcrest.Matcher; @@ -206,7 +207,8 @@ private String mediaType(String version) { } private Version requestWith(String accept, String contentType, String body) { - return compatibleVersionPlugin.getCompatibleVersion(accept, contentType, body.isEmpty() == false); + return compatibleVersionPlugin.getCompatibleVersion(accept, contentType, body.isEmpty() == false, + XContentType.getMediaTypeRegistry()); } } 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..431651e7b1412 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,8 @@ import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.xcontent.MediaType; +import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; @@ -34,8 +36,12 @@ public class RestSqlQueryAction extends BaseRestHandler { - private final SqlMediaTypeParser sqlMediaTypeParser = new SqlMediaTypeParser(); MediaType responseMediaType; + private final SqlMediaTypeParser sqlMediaTypeParser ; + + public RestSqlQueryAction(MediaTypeRegistry additionalMediaTypes) { + sqlMediaTypeParser = new SqlMediaTypeParser(additionalMediaTypes); + } @Override public List routes() { @@ -84,6 +90,8 @@ public RestResponse buildResponse(SqlQueryResponse response) throws Exception { }); } + //we should override + 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 index 189dc137b654c..7f4fc14d8f353 100644 --- 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 @@ -8,26 +8,25 @@ import org.elasticsearch.common.xcontent.MediaType; import org.elasticsearch.common.xcontent.MediaTypeParser; +import org.elasticsearch.common.xcontent.MediaTypeRegistry; 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(); + private MediaTypeParser mediaTypeParser ; + + public SqlMediaTypeParser(MediaTypeRegistry additionalMediaTypes) { + MediaTypeRegistry register = new MediaTypeRegistry() + .register(XContentType.getMediaTypeRegistry()) + .register(additionalMediaTypes); + mediaTypeParser = new MediaTypeParser<>(register); + } + /* * Since we support {@link TextFormat} and @@ -48,19 +47,19 @@ public MediaType getMediaType(RestRequest request, SqlQueryRequest sqlRequest) { // 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))); + return validateColumnarRequest(sqlRequest.columnar(), mediaTypeParser.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)); + return validateColumnarRequest(sqlRequest.columnar(), mediaTypeParser.fromMediaType(accept)); } } String contentType = request.header("Content-Type"); assert contentType != null : "The Content-Type header is required"; - return validateColumnarRequest(sqlRequest.columnar(), parser.fromMediaType(contentType)); + return validateColumnarRequest(sqlRequest.columnar(), mediaTypeParser.fromMediaType(contentType)); } private static MediaType validateColumnarRequest(boolean requestIsColumnar, MediaType 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..24b6973fd298e 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,9 @@ 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.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,6 +47,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.function.Supplier; public class SqlPlugin extends Plugin implements ActionPlugin { @@ -106,7 +109,7 @@ public List getRestHandlers(Settings settings, RestController restC SettingsFilter settingsFilter, IndexNameExpressionResolver indexNameExpressionResolver, Supplier nodesInCluster) { - return Arrays.asList(new RestSqlQueryAction(), + return Arrays.asList(new RestSqlQueryAction(getAdditionalMediaTypes()), new RestSqlTranslateAction(), new RestSqlClearCursorAction(), new RestSqlStatsAction()); @@ -124,4 +127,41 @@ public List getRestHandlers(Settings settings, RestController restC usageAction, infoAction); } + + @Override + public MediaTypeRegistry getAdditionalMediaTypes() { + MediaTypeRegistry mediaTypeRegistry = new MediaTypeRegistry(); + mediaTypeRegistry.register(TextFormat.PLAIN_TEXT.typeWithSubtype(), + TextFormat.PLAIN_TEXT, + TextFormat.PLAIN_TEXT.format(), + Map.of("header", "present|absent", "charset", "utf-8")) + .register(TextFormat.CSV.typeWithSubtype(), + TextFormat.CSV, + TextFormat.CSV.format(), + Map.of("header", "present|absent", "charset", "utf-8", + "delimiter", ".+")) + .register(TextFormat.TSV.typeWithSubtype(), + TextFormat.TSV, + TextFormat.TSV.format(), + Map.of("header", "present|absent", "charset", "utf-8")) + + .register("text/vnd.elasticsearch+plain", + TextFormat.PLAIN_TEXT, + null, + Map.of("header", "present|absent", "charset", "utf-8", + XContentType.COMPATIBLE_WITH_PARAMETER_NAME, XContentType.VERSION_PATTERN)) + .register("text/vnd.elasticsearch+csv", + TextFormat.CSV, + null, + Map.of("header", "present|absent", "charset", "utf-8", + "delimiter", ".+", XContentType.COMPATIBLE_WITH_PARAMETER_NAME, XContentType.VERSION_PATTERN)) + .register("text/vnd.elasticsearch+tsv", + TextFormat.TSV, + null, + Map.of("header", "present|absent", "charset", "utf-8", + XContentType.COMPATIBLE_WITH_PARAMETER_NAME, XContentType.VERSION_PATTERN)); + + return mediaTypeRegistry; + } + } 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 index 0459b777b15f9..fd3e49801655f 100644 --- 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 @@ -6,9 +6,13 @@ package org.elasticsearch.xpack.sql.plugin; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.MediaType; +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.rest.RestRequest; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.rest.FakeRestRequest; @@ -27,7 +31,10 @@ import static org.hamcrest.CoreMatchers.nullValue; public class SqlMediaTypeParserTests extends ESTestCase { - SqlMediaTypeParser parser = new SqlMediaTypeParser(); + SqlMediaTypeParser parser = new SqlMediaTypeParser( + new MediaTypeRegistry() + .register(new SqlPlugin(Settings.EMPTY).getAdditionalMediaTypes()) + .register(XContentType.getMediaTypeRegistry())); public void testPlainTextDetection() { MediaType text = parser.getMediaType(reqWithAccept("text/plain"), createTestInstance(false, Mode.PLAIN, false));