Skip to content

Commit

Permalink
Media-type parser (#61987)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
pgomulka authored Sep 17, 2020
1 parent ae8c0ce commit 86ba732
Show file tree
Hide file tree
Showing 26 changed files with 396 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ static String convertResponseToJson(Response response) throws IOException {
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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1899,7 +1899,7 @@ protected final <Resp> Resp parseEntity(final HttpEntity entity,
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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ public void testUpdate() throws IOException {

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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@
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.
Expand All @@ -47,7 +46,7 @@ public String mediaType() {
}

@Override
public String shortName() {
public String subtype() {
return "json";
}

Expand All @@ -66,7 +65,7 @@ public String mediaTypeWithoutParameters() {
}

@Override
public String shortName() {
public String subtype() {
return "smile";
}

Expand All @@ -85,7 +84,7 @@ public String mediaTypeWithoutParameters() {
}

@Override
public String shortName() {
public String subtype() {
return "yaml";
}

Expand All @@ -104,7 +103,7 @@ public String mediaTypeWithoutParameters() {
}

@Override
public String shortName() {
public String subtype() {
return "cbor";
}

Expand All @@ -114,54 +113,30 @@ public XContent xContent() {
}
};

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;

Expand All @@ -177,10 +152,19 @@ public String mediaType() {
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();
}
}
Loading

0 comments on commit 86ba732

Please sign in to comment.