Răsfoiți Sursa

Media-type parser (#61987)

Splitting method XContentType.fromMediaTypeOrFormat into two separate methods. This will help to validate media type provided in Accept or Content-Type headers.
Extract parsing logic from XContentType (fromMediaType and fromFormat methods) to a separate MediaTypeParser class. This will help reuse the same parsing logic for XContentType and TextFormat (used in sql)

`Media-Types type/subtype; parameters` parsing is in defined https://tools.ietf.org/html/rfc7231#section-3.1.1.1

part of  #61427
Przemyslaw Gomulka 5 ani în urmă
părinte
comite
86ba7324c8
26 a modificat fișierele cu 396 adăugiri și 112 ștergeri
  1. 1 1
      client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseClient.java
  2. 1 1
      client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java
  3. 1 1
      client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PostDataRequest.java
  4. 1 1
      client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java
  5. 52 0
      libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaType.java
  6. 123 0
      libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeParser.java
  7. 31 47
      libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentType.java
  8. 71 0
      libs/x-content/src/test/java/org/elasticsearch/common/xcontent/MediaTypeParserTests.java
  9. 8 2
      server/src/main/java/org/elasticsearch/rest/AbstractRestChannel.java
  10. 8 1
      server/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java
  11. 30 21
      server/src/test/java/org/elasticsearch/common/xcontent/XContentTypeTests.java
  12. 1 1
      server/src/test/java/org/elasticsearch/rest/BytesRestResponseTests.java
  13. 4 4
      test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java
  14. 1 1
      test/framework/src/main/java/org/elasticsearch/test/rest/yaml/ClientYamlTestResponse.java
  15. 1 1
      test/framework/src/main/java/org/elasticsearch/test/rest/yaml/ObjectPath.java
  16. 1 1
      x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/TimeSeriesRestDriver.java
  17. 1 1
      x-pack/plugin/sql/sql-client/src/main/java/org/elasticsearch/xpack/sql/client/HttpClient.java
  18. 10 2
      x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/RestSqlQueryAction.java
  19. 43 19
      x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/TextFormat.java
  20. 1 1
      x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpResponse.java
  21. 1 1
      x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/text/TextTemplate.java
  22. 1 1
      x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/text/TextTemplateEngine.java
  23. 1 1
      x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/input/http/ExecutableHttpInput.java
  24. 1 1
      x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/support/WatcherTemplateTests.java
  25. 1 1
      x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java
  26. 1 1
      x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/TransformSurvivesUpgradeIT.java

+ 1 - 1
client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseClient.java

@@ -217,7 +217,7 @@ public final class LicenseClient {
         if (entity.getContentType() == null) {
             throw new IllegalStateException("Elasticsearch didn't return the [Content-Type] header, unable to parse response body");
         }
-        XContentType xContentType = XContentType.fromMediaTypeOrFormat(entity.getContentType().getValue());
+        XContentType xContentType = XContentType.fromMediaType(entity.getContentType().getValue());
         if (xContentType == null) {
             throw new IllegalStateException("Unsupported Content-Type: " + entity.getContentType().getValue());
         }

+ 1 - 1
client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java

@@ -1899,7 +1899,7 @@ public class RestHighLevelClient implements Closeable {
         if (entity.getContentType() == null) {
             throw new IllegalStateException("Elasticsearch didn't return the [Content-Type] header, unable to parse response body");
         }
-        XContentType xContentType = XContentType.fromMediaTypeOrFormat(entity.getContentType().getValue());
+        XContentType xContentType = XContentType.fromMediaType(entity.getContentType().getValue());
         if (xContentType == null) {
             throw new IllegalStateException("Unsupported Content-Type: " + entity.getContentType().getValue());
         }

+ 1 - 1
client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PostDataRequest.java

@@ -47,7 +47,7 @@ public class PostDataRequest implements Validatable, ToXContentObject {
 
     public static final ConstructingObjectParser<PostDataRequest, Void> PARSER =
         new ConstructingObjectParser<>("post_data_request",
-            (a) -> new PostDataRequest((String)a[0], XContentType.fromMediaTypeOrFormat((String)a[1]), new byte[0]));
+            (a) -> new PostDataRequest((String)a[0], XContentType.fromMediaType((String)a[1]), new byte[0]));
 
     static {
         PARSER.declareString(ConstructingObjectParser.constructorArg(), Job.ID);

+ 1 - 1
client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java

@@ -767,7 +767,7 @@ public class RequestConvertersTests extends ESTestCase {
 
         UpdateRequest parsedUpdateRequest = new UpdateRequest();
 
-        XContentType entityContentType = XContentType.fromMediaTypeOrFormat(entity.getContentType().getValue());
+        XContentType entityContentType = XContentType.fromMediaType(entity.getContentType().getValue());
         try (XContentParser parser = createParser(entityContentType.xContent(), entity.getContent())) {
             parsedUpdateRequest.fromXContent(parser);
         }

+ 52 - 0
libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaType.java

@@ -0,0 +1,52 @@
+/*
+ * 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;
+
+/**
+ * Abstracts a <a href="http://en.wikipedia.org/wiki/Internet_media_type">Media Type</a> and a format parameter.
+ * Media types are used as values on Content-Type and Accept headers
+ * format is an URL parameter, specifies response media type.
+ */
+public interface MediaType {
+    /**
+     * Returns a type part of a MediaType
+     * i.e. application for application/json
+     */
+    String type();
+
+    /**
+     * Returns a subtype part of a MediaType.
+     * i.e. json for application/json
+     */
+    String subtype();
+
+    /**
+     * Returns a corresponding format for a MediaType. i.e. json for application/json media type
+     * Can differ from the MediaType's subtype i.e plain/text has a subtype of text but format is txt
+     */
+    String format();
+
+    /**
+     * returns a string representation of a media type.
+     */
+    default String typeWithSubtype(){
+        return type() + "/" + subtype();
+    }
+}

+ 123 - 0
libs/x-content/src/main/java/org/elasticsearch/common/xcontent/MediaTypeParser.java

@@ -0,0 +1,123 @@
+/*
+ * 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.Locale;
+import java.util.Map;
+
+public class MediaTypeParser<T extends MediaType> {
+    private final Map<String, T> formatToMediaType;
+    private final Map<String, T> typeWithSubtypeToMediaType;
+
+    public MediaTypeParser(T[] acceptedMediaTypes) {
+        this(acceptedMediaTypes, Map.of());
+    }
+
+    public MediaTypeParser(T[] acceptedMediaTypes, Map<String, T> additionalMediaTypes) {
+        final int size = acceptedMediaTypes.length + additionalMediaTypes.size();
+        Map<String, T> formatMap = new HashMap<>(size);
+        Map<String, T> typeMap = new HashMap<>(size);
+        for (T mediaType : acceptedMediaTypes) {
+            typeMap.put(mediaType.typeWithSubtype(), mediaType);
+            formatMap.put(mediaType.format(), mediaType);
+        }
+        for (Map.Entry<String, T> entry : additionalMediaTypes.entrySet()) {
+            String typeWithSubtype = entry.getKey();
+            T mediaType = entry.getValue();
+
+            typeMap.put(typeWithSubtype.toLowerCase(Locale.ROOT), mediaType);
+            formatMap.put(mediaType.format(), mediaType);
+        }
+
+        this.formatToMediaType = Map.copyOf(formatMap);
+        this.typeWithSubtypeToMediaType = Map.copyOf(typeMap);
+    }
+
+    public T fromMediaType(String mediaType) {
+        ParsedMediaType parsedMediaType = parseMediaType(mediaType);
+        return parsedMediaType != null ? parsedMediaType.getMediaType() : null;
+    }
+
+    public T fromFormat(String format) {
+        if (format == null) {
+            return null;
+        }
+        return formatToMediaType.get(format.toLowerCase(Locale.ROOT));
+    }
+
+    /**
+     * parsing media type that follows https://tools.ietf.org/html/rfc7231#section-3.1.1.1
+     * @param headerValue a header value from Accept or Content-Type
+     * @return a parsed media-type
+     */
+    public ParsedMediaType parseMediaType(String headerValue) {
+        if (headerValue != null) {
+            String[] split = headerValue.toLowerCase(Locale.ROOT).split(";");
+
+            String[] typeSubtype = split[0].trim().toLowerCase(Locale.ROOT)
+                .split("/");
+            if (typeSubtype.length == 2) {
+                String type = typeSubtype[0];
+                String subtype = typeSubtype[1];
+                T xContentType = typeWithSubtypeToMediaType.get(type + "/" + subtype);
+                if (xContentType != null) {
+                    Map<String, String> parameters = new HashMap<>();
+                    for (int i = 1; i < split.length; i++) {
+                        //spaces are allowed between parameters, but not between '=' sign
+                        String[] keyValueParam = split[i].trim().split("=");
+                        if (keyValueParam.length != 2 || hasSpaces(keyValueParam[0]) || hasSpaces(keyValueParam[1])) {
+                            return null;
+                        }
+                        parameters.put(keyValueParam[0].toLowerCase(Locale.ROOT), keyValueParam[1].toLowerCase(Locale.ROOT));
+                    }
+                    return new ParsedMediaType(xContentType, parameters);
+                }
+            }
+
+        }
+        return null;
+    }
+
+    private boolean hasSpaces(String s) {
+        return s.trim().equals(s) == false;
+    }
+
+    /**
+     * A media type object that contains all the information provided on a Content-Type or Accept header
+     */
+    public class ParsedMediaType {
+        private final Map<String, String> parameters;
+        private final T mediaType;
+
+        public ParsedMediaType(T mediaType, Map<String, String> parameters) {
+            this.parameters = parameters;
+            this.mediaType = mediaType;
+        }
+
+        public T getMediaType() {
+            return mediaType;
+        }
+
+        public Map<String, String> getParameters() {
+            return parameters;
+        }
+    }
+}

+ 31 - 47
libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentType.java

@@ -24,13 +24,12 @@ import org.elasticsearch.common.xcontent.json.JsonXContent;
 import org.elasticsearch.common.xcontent.smile.SmileXContent;
 import org.elasticsearch.common.xcontent.yaml.YamlXContent;
 
-import java.util.Locale;
-import java.util.Objects;
+import java.util.Map;
 
 /**
  * The content type of {@link org.elasticsearch.common.xcontent.XContent}.
  */
-public enum XContentType {
+public enum XContentType implements MediaType {
 
     /**
      * A JSON based content type.
@@ -47,7 +46,7 @@ public enum XContentType {
         }
 
         @Override
-        public String shortName() {
+        public String subtype() {
             return "json";
         }
 
@@ -66,7 +65,7 @@ public enum XContentType {
         }
 
         @Override
-        public String shortName() {
+        public String subtype() {
             return "smile";
         }
 
@@ -85,7 +84,7 @@ public enum XContentType {
         }
 
         @Override
-        public String shortName() {
+        public String subtype() {
             return "yaml";
         }
 
@@ -104,7 +103,7 @@ public enum XContentType {
         }
 
         @Override
-        public String shortName() {
+        public String subtype() {
             return "cbor";
         }
 
@@ -114,54 +113,30 @@ public enum XContentType {
         }
     };
 
+    public static final MediaTypeParser<XContentType> mediaTypeParser = new MediaTypeParser<>(XContentType.values(),
+        Map.of("application/*", JSON, "application/x-ndjson", JSON));
+
+
     /**
-     * Accepts either a format string, which is equivalent to {@link XContentType#shortName()} or a media type that optionally has
-     * parameters and attempts to match the value to an {@link XContentType}. The comparisons are done in lower case format and this method
-     * also supports a wildcard accept for {@code application/*}. This method can be used to parse the {@code Accept} HTTP header or a
-     * format query string parameter. This method will return {@code null} if no match is found
+     * Accepts a format string, which is most of the time is equivalent to {@link XContentType#subtype()}
+     * and attempts to match the value to an {@link XContentType}.
+     * The comparisons are done in lower case format.
+     * This method will return {@code null} if no match is found
      */
-    public static XContentType fromMediaTypeOrFormat(String mediaType) {
-        if (mediaType == null) {
-            return null;
-        }
-        for (XContentType type : values()) {
-            if (isSameMediaTypeOrFormatAs(mediaType, type)) {
-                return type;
-            }
-        }
-        final String lowercaseMediaType = mediaType.toLowerCase(Locale.ROOT);
-        if (lowercaseMediaType.startsWith("application/*")) {
-            return JSON;
-        }
-
-        return null;
+    public static XContentType fromFormat(String mediaType) {
+        return mediaTypeParser.fromFormat(mediaType);
     }
 
     /**
      * Attempts to match the given media type with the known {@link XContentType} values. This match is done in a case-insensitive manner.
-     * The provided media type should not include any parameters. This method is suitable for parsing part of the {@code Content-Type}
-     * HTTP header. This method will return {@code null} if no match is found
+     * The provided media type can optionally has parameters.
+     * This method is suitable for parsing of the {@code Content-Type} and {@code Accept} HTTP headers.
+     * This method will return {@code null} if no match is found
      */
-    public static XContentType fromMediaType(String mediaType) {
-        final String lowercaseMediaType = Objects.requireNonNull(mediaType, "mediaType cannot be null").toLowerCase(Locale.ROOT);
-        for (XContentType type : values()) {
-            if (type.mediaTypeWithoutParameters().equals(lowercaseMediaType)) {
-                return type;
-            }
-        }
-        // we also support newline delimited JSON: http://specs.okfnlabs.org/ndjson/
-        if (lowercaseMediaType.toLowerCase(Locale.ROOT).equals("application/x-ndjson")) {
-            return XContentType.JSON;
-        }
-
-        return null;
+    public static XContentType fromMediaType(String mediaTypeHeaderValue) {
+        return mediaTypeParser.fromMediaType(mediaTypeHeaderValue);
     }
 
-    private static boolean isSameMediaTypeOrFormatAs(String stringType, XContentType type) {
-        return type.mediaTypeWithoutParameters().equalsIgnoreCase(stringType) ||
-                stringType.toLowerCase(Locale.ROOT).startsWith(type.mediaTypeWithoutParameters().toLowerCase(Locale.ROOT) + ";") ||
-                type.shortName().equalsIgnoreCase(stringType);
-    }
 
     private int index;
 
@@ -177,10 +152,19 @@ public enum XContentType {
         return mediaTypeWithoutParameters();
     }
 
-    public abstract String shortName();
 
     public abstract XContent xContent();
 
     public abstract String mediaTypeWithoutParameters();
 
+
+    @Override
+    public String type() {
+        return "application";
+    }
+
+    @Override
+    public String format() {
+        return subtype();
+    }
 }

+ 71 - 0
libs/x-content/src/test/java/org/elasticsearch/common/xcontent/MediaTypeParserTests.java

@@ -0,0 +1,71 @@
+/*
+ * 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.test.ESTestCase;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+
+public class MediaTypeParserTests extends ESTestCase {
+    MediaTypeParser<XContentType> mediaTypeParser = XContentType.mediaTypeParser;
+
+    public void testJsonWithParameters() throws Exception {
+        String mediaType = "application/json";
+        assertThat(mediaTypeParser.parseMediaType(mediaType).getParameters(),
+            equalTo(Collections.emptyMap()));
+        assertThat(mediaTypeParser.parseMediaType(mediaType + ";").getParameters(),
+            equalTo(Collections.emptyMap()));
+        assertThat(mediaTypeParser.parseMediaType(mediaType + "; charset=UTF-8").getParameters(),
+            equalTo(Map.of("charset", "utf-8")));
+        assertThat(mediaTypeParser.parseMediaType(mediaType + "; custom=123;charset=UTF-8").getParameters(),
+            equalTo(Map.of("charset", "utf-8", "custom", "123")));
+    }
+
+    public void testWhiteSpaceInTypeSubtype() {
+        String mediaType = " application/json ";
+        assertThat(mediaTypeParser.parseMediaType(mediaType).getMediaType(),
+            equalTo(XContentType.JSON));
+
+        assertThat(mediaTypeParser.parseMediaType(mediaType + "; custom=123; charset=UTF-8").getParameters(),
+            equalTo(Map.of("charset", "utf-8", "custom", "123")));
+        assertThat(mediaTypeParser.parseMediaType(mediaType + "; custom=123;\n charset=UTF-8").getParameters(),
+            equalTo(Map.of("charset", "utf-8", "custom", "123")));
+
+        mediaType = " application / json ";
+        assertThat(mediaTypeParser.parseMediaType(mediaType),
+            is(nullValue()));
+    }
+
+    public void testInvalidParameters() {
+        String mediaType = "application/json";
+        assertThat(mediaTypeParser.parseMediaType(mediaType + "; keyvalueNoEqualsSign"),
+            is(nullValue()));
+
+        assertThat(mediaTypeParser.parseMediaType(mediaType + "; key = value"),
+            is(nullValue()));
+        assertThat(mediaTypeParser.parseMediaType(mediaType + "; key=") ,
+            is(nullValue()));
+    }
+}

+ 8 - 2
server/src/main/java/org/elasticsearch/rest/AbstractRestChannel.java

@@ -45,6 +45,7 @@ public abstract class AbstractRestChannel implements RestChannel {
     private final String filterPath;
     private final boolean pretty;
     private final boolean human;
+    private final String acceptHeader;
 
     private BytesStreamOutput bytesOut;
 
@@ -58,7 +59,8 @@ public abstract class AbstractRestChannel implements RestChannel {
     protected AbstractRestChannel(RestRequest request, boolean detailedErrorsEnabled) {
         this.request = request;
         this.detailedErrorsEnabled = detailedErrorsEnabled;
-        this.format = request.param("format", request.header("Accept"));
+        this.format = request.param("format");
+        this.acceptHeader = request.header("Accept");
         this.filterPath = request.param("filter_path", null);
         this.pretty = request.paramAsBoolean("pretty", false);
         this.human = request.paramAsBoolean("human", false);
@@ -96,7 +98,11 @@ public abstract class AbstractRestChannel implements RestChannel {
     public XContentBuilder newBuilder(@Nullable XContentType requestContentType, @Nullable XContentType responseContentType,
             boolean useFiltering) throws IOException {
         if (responseContentType == null) {
-            responseContentType = XContentType.fromMediaTypeOrFormat(format);
+            //TODO PG shoudld format vs acceptHeader be always the same, do we allow overriding?
+            responseContentType = XContentType.fromFormat(format);
+            if (responseContentType == null) {
+                responseContentType = XContentType.fromMediaType(acceptHeader);
+            }
         }
         // try to determine the response content type from the media type or the format query string parameter, with the format parameter
         // taking precedence over the Accept header

+ 8 - 1
server/src/main/java/org/elasticsearch/rest/action/cat/RestTable.java

@@ -51,13 +51,20 @@ public class RestTable {
 
     public static RestResponse buildResponse(Table table, RestChannel channel) throws Exception {
         RestRequest request = channel.request();
-        XContentType xContentType = XContentType.fromMediaTypeOrFormat(request.param("format", request.header("Accept")));
+        XContentType xContentType = getxContentType(request);
         if (xContentType != null) {
             return buildXContentBuilder(table, channel);
         }
         return buildTextPlainResponse(table, channel);
     }
 
+    private static XContentType getxContentType(RestRequest request) {
+        if (request.hasParam("format")) {
+            return XContentType.fromFormat(request.param("format"));
+        }
+        return XContentType.fromMediaType(request.header("Accept"));
+    }
+
     public static RestResponse buildXContentBuilder(Table table, RestChannel channel) throws Exception {
         RestRequest request = channel.request();
         XContentBuilder builder = channel.newBuilder();

+ 30 - 21
server/src/test/java/org/elasticsearch/common/xcontent/XContentTypeTests.java

@@ -26,62 +26,71 @@ import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.nullValue;
 
 public class XContentTypeTests extends ESTestCase {
+
     public void testFromJson() throws Exception {
         String mediaType = "application/json";
         XContentType expectedXContentType = XContentType.JSON;
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType), equalTo(expectedXContentType));
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType + ";"), equalTo(expectedXContentType));
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType + "; charset=UTF-8"), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + ";"), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + "; charset=UTF-8"), equalTo(expectedXContentType));
+    }
+
+    public void testFromNdJson() throws Exception {
+        String mediaType = "application/x-ndjson";
+        XContentType expectedXContentType = XContentType.JSON;
+        assertThat(XContentType.fromMediaType(mediaType), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + ";"), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + "; charset=UTF-8"), equalTo(expectedXContentType));
     }
 
     public void testFromJsonUppercase() throws Exception {
         String mediaType = "application/json".toUpperCase(Locale.ROOT);
         XContentType expectedXContentType = XContentType.JSON;
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType), equalTo(expectedXContentType));
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType + ";"), equalTo(expectedXContentType));
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType + "; charset=UTF-8"), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + ";"), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + "; charset=UTF-8"), equalTo(expectedXContentType));
     }
 
     public void testFromYaml() throws Exception {
         String mediaType = "application/yaml";
         XContentType expectedXContentType = XContentType.YAML;
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType), equalTo(expectedXContentType));
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType + ";"), equalTo(expectedXContentType));
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType + "; charset=UTF-8"), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + ";"), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + "; charset=UTF-8"), equalTo(expectedXContentType));
     }
 
     public void testFromSmile() throws Exception {
         String mediaType = "application/smile";
         XContentType expectedXContentType = XContentType.SMILE;
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType), equalTo(expectedXContentType));
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType + ";"), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + ";"), equalTo(expectedXContentType));
     }
 
     public void testFromCbor() throws Exception {
         String mediaType = "application/cbor";
         XContentType expectedXContentType = XContentType.CBOR;
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType), equalTo(expectedXContentType));
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType + ";"), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + ";"), equalTo(expectedXContentType));
     }
 
     public void testFromWildcard() throws Exception {
         String mediaType = "application/*";
         XContentType expectedXContentType = XContentType.JSON;
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType), equalTo(expectedXContentType));
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType + ";"), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + ";"), equalTo(expectedXContentType));
     }
 
     public void testFromWildcardUppercase() throws Exception {
         String mediaType = "APPLICATION/*";
         XContentType expectedXContentType = XContentType.JSON;
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType), equalTo(expectedXContentType));
-        assertThat(XContentType.fromMediaTypeOrFormat(mediaType + ";"), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType), equalTo(expectedXContentType));
+        assertThat(XContentType.fromMediaType(mediaType + ";"), equalTo(expectedXContentType));
     }
 
     public void testFromRubbish() throws Exception {
-        assertThat(XContentType.fromMediaTypeOrFormat(null), nullValue());
-        assertThat(XContentType.fromMediaTypeOrFormat(""), nullValue());
-        assertThat(XContentType.fromMediaTypeOrFormat("text/plain"), nullValue());
-        assertThat(XContentType.fromMediaTypeOrFormat("gobbly;goop"), nullValue());
+        assertThat(XContentType.fromMediaType(null), nullValue());
+        assertThat(XContentType.fromMediaType(""), nullValue());
+        assertThat(XContentType.fromMediaType("text/plain"), nullValue());
+        assertThat(XContentType.fromMediaType("gobbly;goop"), nullValue());
     }
 }

+ 1 - 1
server/src/test/java/org/elasticsearch/rest/BytesRestResponseTests.java

@@ -299,7 +299,7 @@ public class BytesRestResponseTests extends ESTestCase {
 
         final XContentType xContentType = randomFrom(XContentType.values());
 
-        Map<String, String> params = Collections.singletonMap("format", xContentType.mediaType());
+        Map<String, String> params = Collections.singletonMap("format", xContentType.format());
         RestRequest request = new FakeRestRequest.Builder(xContentRegistry()).withParams(params).build();
         RestChannel channel = detailed ? new DetailedExceptionRestChannel(request) : new SimpleExceptionRestChannel(request);
 

+ 4 - 4
test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java

@@ -121,7 +121,7 @@ public abstract class ESRestTestCase extends ESTestCase {
      * Convert the entity from a {@link Response} into a map of maps.
      */
     public static Map<String, Object> entityAsMap(Response response) throws IOException {
-        XContentType xContentType = XContentType.fromMediaTypeOrFormat(response.getEntity().getContentType().getValue());
+        XContentType xContentType = XContentType.fromMediaType(response.getEntity().getContentType().getValue());
         // EMPTY and THROW are fine here because `.map` doesn't use named x content or deprecation
         try (XContentParser parser = xContentType.xContent().createParser(
                 NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION,
@@ -134,7 +134,7 @@ public abstract class ESRestTestCase extends ESTestCase {
      * Convert the entity from a {@link Response} into a list of maps.
      */
     public static List<Object> entityAsList(Response response) throws IOException {
-        XContentType xContentType = XContentType.fromMediaTypeOrFormat(response.getEntity().getContentType().getValue());
+        XContentType xContentType = XContentType.fromMediaType(response.getEntity().getContentType().getValue());
         // EMPTY and THROW are fine here because `.map` doesn't use named x content or deprecation
         try (XContentParser parser = xContentType.xContent().createParser(
             NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION,
@@ -1192,7 +1192,7 @@ public abstract class ESRestTestCase extends ESTestCase {
     }
 
     protected static Map<String, Object> responseAsMap(Response response) throws IOException {
-        XContentType entityContentType = XContentType.fromMediaTypeOrFormat(response.getEntity().getContentType().getValue());
+        XContentType entityContentType = XContentType.fromMediaType(response.getEntity().getContentType().getValue());
         Map<String, Object> responseEntity = XContentHelper.convertToMap(entityContentType.xContent(),
                 response.getEntity().getContent(), false);
         assertNotNull(responseEntity);
@@ -1469,7 +1469,7 @@ public abstract class ESRestTestCase extends ESTestCase {
             assertOK(response);
 
             try (InputStream is = response.getEntity().getContent()) {
-                XContentType xContentType = XContentType.fromMediaTypeOrFormat(response.getEntity().getContentType().getValue());
+                XContentType xContentType = XContentType.fromMediaType(response.getEntity().getContentType().getValue());
                 final Map<String, ?> map = XContentHelper.convertToMap(xContentType.xContent(), is, true);
                 assertThat(map, notNullValue());
                 assertThat("License must exist", map.containsKey("license"), equalTo(true));

+ 1 - 1
test/framework/src/main/java/org/elasticsearch/test/rest/yaml/ClientYamlTestResponse.java

@@ -53,7 +53,7 @@ public class ClientYamlTestResponse {
         this.response = response;
         if (response.getEntity() != null) {
             String contentType = response.getHeader("Content-Type");
-            this.bodyContentType = XContentType.fromMediaTypeOrFormat(contentType);
+            this.bodyContentType = XContentType.fromMediaType(contentType);
             try {
                 byte[] bytes = EntityUtils.toByteArray(response.getEntity());
                 //skip parsing if we got text back (e.g. if we called _cat apis)

+ 1 - 1
test/framework/src/main/java/org/elasticsearch/test/rest/yaml/ObjectPath.java

@@ -44,7 +44,7 @@ public class ObjectPath {
     public static ObjectPath createFromResponse(Response response) throws IOException {
         byte[] bytes = EntityUtils.toByteArray(response.getEntity());
         String contentType = response.getHeader("Content-Type");
-        XContentType xContentType = XContentType.fromMediaTypeOrFormat(contentType);
+        XContentType xContentType = XContentType.fromMediaType(contentType);
         return ObjectPath.createFromXContent(xContentType.xContent(), new BytesArray(bytes));
     }
 

+ 1 - 1
x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/TimeSeriesRestDriver.java

@@ -231,7 +231,7 @@ public final class TimeSeriesRestDriver {
     @SuppressWarnings("unchecked")
     public static Integer getNumberOfSegments(RestClient client, String index) throws IOException {
         Response response = client.performRequest(new Request("GET", index + "/_segments"));
-        XContentType entityContentType = XContentType.fromMediaTypeOrFormat(response.getEntity().getContentType().getValue());
+        XContentType entityContentType = XContentType.fromMediaType(response.getEntity().getContentType().getValue());
         Map<String, Object> responseEntity = XContentHelper.convertToMap(entityContentType.xContent(),
             response.getEntity().getContent(), false);
         responseEntity = (Map<String, Object>) responseEntity.get("indices");

+ 1 - 1
x-pack/plugin/sql/sql-client/src/main/java/org/elasticsearch/xpack/sql/client/HttpClient.java

@@ -158,7 +158,7 @@ public class HttpClient {
 
     private Tuple<XContentType, byte[]> readFrom(InputStream inputStream, Function<String, String> headers) {
         String contentType = headers.apply("Content-Type");
-        XContentType xContentType = XContentType.fromMediaTypeOrFormat(contentType);
+        XContentType xContentType = XContentType.fromMediaType(contentType);
         if (xContentType == null) {
             throw new IllegalStateException("Unsupported Content-Type: " + contentType);
         }

+ 10 - 2
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/RestSqlQueryAction.java

@@ -64,7 +64,7 @@ public class RestSqlQueryAction extends BaseRestHandler {
          * 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.
          */
-        String accept = null;
+        String accept;
 
         if (Mode.isDedicatedClient(sqlRequest.requestInfo().mode())
                 && (sqlRequest.binaryCommunication() == null || sqlRequest.binaryCommunication())) {
@@ -92,7 +92,7 @@ public class RestSqlQueryAction extends BaseRestHandler {
          * that doesn't parse it'll throw an {@link IllegalArgumentException}
          * which we turn into a 400 error.
          */
-        XContentType xContentType = accept == null ? XContentType.JSON : XContentType.fromMediaTypeOrFormat(accept);
+        XContentType xContentType = getXContentType(accept);
         textFormat = xContentType == null ? TextFormat.fromMediaTypeOrFormat(accept) : null;
 
         if (xContentType == null && sqlRequest.columnar()) {
@@ -130,6 +130,14 @@ public class RestSqlQueryAction extends BaseRestHandler {
         });
     }
 
+    private XContentType getXContentType(String accept) {
+        if (accept == null) {
+            return XContentType.JSON;
+        }
+        XContentType xContentType = XContentType.fromFormat(accept);
+        return xContentType != null ? xContentType : XContentType.fromMediaType(accept);
+    }
+
     @Override
     protected Set<String> responseParams() {
         return textFormat == TextFormat.CSV ? Collections.singleton(URL_PARAM_DELIMITER) : Collections.emptySet();

+ 43 - 19
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plugin/TextFormat.java

@@ -7,6 +7,8 @@ package org.elasticsearch.xpack.sql.plugin;
 
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.xcontent.MediaType;
+import org.elasticsearch.common.xcontent.MediaTypeParser;
 import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.xpack.ql.util.StringUtils;
 import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
@@ -32,7 +34,7 @@ import static org.elasticsearch.xpack.sql.proto.Protocol.URL_PARAM_DELIMITER;
 /**
  * Templating class for displaying SQL responses in text formats.
  */
-enum TextFormat {
+enum TextFormat implements MediaType {
 
     /**
      * Default text writer.
@@ -82,7 +84,7 @@ enum TextFormat {
         }
 
         @Override
-        String shortName() {
+        public String format() {
             return FORMAT_TEXT;
         }
 
@@ -100,6 +102,13 @@ enum TextFormat {
         protected String eol() {
             throw new UnsupportedOperationException();
         }
+
+        @Override
+        public String subtype() {
+            return "plain";
+        }
+
+
     },
 
     /**
@@ -124,7 +133,7 @@ enum TextFormat {
         }
 
         @Override
-        String shortName() {
+        public String format() {
             return FORMAT_CSV;
         }
 
@@ -214,6 +223,11 @@ enum TextFormat {
                 return !header.toLowerCase(Locale.ROOT).equals(PARAM_HEADER_ABSENT);
             }
         }
+
+        @Override
+        public String subtype() {
+            return "csv";
+        }
     },
 
     TSV() {
@@ -229,7 +243,7 @@ enum TextFormat {
         }
 
         @Override
-        String shortName() {
+        public String format() {
             return FORMAT_TSV;
         }
 
@@ -263,6 +277,11 @@ enum TextFormat {
 
             return sb.toString();
         }
+
+        @Override
+        public String subtype() {
+            return "tab-separated-values";
+        }
     };
 
     private static final String FORMAT_TEXT = "txt";
@@ -275,6 +294,8 @@ enum TextFormat {
     private static final String PARAM_HEADER_ABSENT = "absent";
     private static final String PARAM_HEADER_PRESENT = "present";
 
+    private static final MediaTypeParser<TextFormat> parser = new MediaTypeParser<>(TextFormat.values());
+
     String format(RestRequest request, SqlQueryResponse response) {
         StringBuilder sb = new StringBuilder();
 
@@ -296,25 +317,17 @@ enum TextFormat {
     }
 
     static TextFormat fromMediaTypeOrFormat(String accept) {
-        for (TextFormat text : values()) {
-            String contentType = text.contentType();
-            if (contentType.equalsIgnoreCase(accept)
-                    || accept.toLowerCase(Locale.ROOT).startsWith(contentType + ";")
-                    || text.shortName().equalsIgnoreCase(accept)) {
-                return text;
-            }
+        TextFormat textFormat = parser.fromFormat(accept);
+        if (textFormat != null) {
+            return textFormat;
+        }
+        textFormat = parser.fromMediaType(accept);
+        if (textFormat != null) {
+            return textFormat;
         }
-
         throw new IllegalArgumentException("invalid format [" + accept + "]");
     }
 
-    /**
-     * Short name typically used by format parameter.
-     * Can differ from the IANA mime type.
-     */
-    abstract String shortName();
-
-
     /**
      * Formal IANA mime type.
      */
@@ -360,4 +373,15 @@ enum TextFormat {
     String maybeEscape(String value, Character delimiter) {
         return value;
     }
+
+
+    @Override
+    public String type() {
+        return "text";
+    }
+
+    @Override
+    public String typeWithSubtype() {
+        return contentType();
+    }
 }

+ 1 - 1
x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/http/HttpResponse.java

@@ -105,7 +105,7 @@ public class HttpResponse implements ToXContentObject {
         if (values == null || values.length == 0) {
             return null;
         }
-        return XContentType.fromMediaTypeOrFormat(values[0]);
+        return XContentType.fromMediaType(values[0]);
     }
 
     @Override

+ 1 - 1
x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/text/TextTemplate.java

@@ -89,7 +89,7 @@ public class TextTemplate implements ToXContent {
             return null;
         }
 
-        return XContentType.fromMediaTypeOrFormat(mediaType);
+        return XContentType.fromMediaType(mediaType);
     }
 
     public ScriptType getType() {

+ 1 - 1
x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/common/text/TextTemplateEngine.java

@@ -82,7 +82,7 @@ public class TextTemplateEngine {
             //There must be a __<content_type__:: prefix so the minimum length before detecting '__::' is 3
             int endOfContentName = content.indexOf("__::", 3);
             if (endOfContentName != -1) {
-                return XContentType.fromMediaTypeOrFormat(content.substring(2, endOfContentName));
+                return XContentType.fromFormat(content.substring(2, endOfContentName));
             }
         }
         return null;

+ 1 - 1
x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/input/http/ExecutableHttpInput.java

@@ -99,7 +99,7 @@ public class ExecutableHttpInput extends ExecutableInput<HttpInput, HttpInput.Re
                 }
             } catch (Exception e) {
                 throw new ElasticsearchParseException("could not parse response body [{}] it does not appear to be [{}]", type(), ctx.id(),
-                        response.body().utf8ToString(), contentType.shortName());
+                        response.body().utf8ToString(), contentType.format());
             }
         } else {
             payloadMap.put("_value", response.body().utf8ToString());

+ 1 - 1
x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/support/WatcherTemplateTests.java

@@ -160,7 +160,7 @@ public class WatcherTemplateTests extends ESTestCase {
             return template;
         }
         return new StringBuilder("__")
-                .append(contentType.shortName().toLowerCase(Locale.ROOT))
+                .append(contentType.format().toLowerCase(Locale.ROOT))
                 .append("__::")
                 .append(template)
                 .toString();

+ 1 - 1
x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java

@@ -327,7 +327,7 @@ public class FullClusterRestartIT extends AbstractFullClusterRestartTestCase {
 
         if (isRunningAgainstOldCluster() == false) {
             Response response = client().performRequest(new Request("GET", "_slm/stats"));
-            XContentType xContentType = XContentType.fromMediaTypeOrFormat(response.getEntity().getContentType().getValue());
+            XContentType xContentType = XContentType.fromMediaType(response.getEntity().getContentType().getValue());
             try (XContentParser parser = xContentType.xContent().createParser(NamedXContentRegistry.EMPTY,
                 DeprecationHandler.THROW_UNSUPPORTED_OPERATION, response.getEntity().getContent())) {
                 assertEquals(new SnapshotLifecycleStats(), SnapshotLifecycleStats.parse(parser));

+ 1 - 1
x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/TransformSurvivesUpgradeIT.java

@@ -331,7 +331,7 @@ public class TransformSurvivesUpgradeIT extends AbstractUpgradeTestCase {
         final Request getStats = new Request("GET", getTransformEndpoint() + id + "/_stats");
         Response response = client().performRequest(getStats);
         assertEquals(200, response.getStatusLine().getStatusCode());
-        XContentType xContentType = XContentType.fromMediaTypeOrFormat(response.getEntity().getContentType().getValue());
+        XContentType xContentType = XContentType.fromMediaType(response.getEntity().getContentType().getValue());
         try (XContentParser parser = xContentType.xContent().createParser(
             NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION,
             response.getEntity().getContent())) {