Skip to content

Commit 86ba732

Browse files
authored
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
1 parent ae8c0ce commit 86ba732

File tree

26 files changed

+396
-112
lines changed

26 files changed

+396
-112
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ static String convertResponseToJson(Response response) throws IOException {
217217
if (entity.getContentType() == null) {
218218
throw new IllegalStateException("Elasticsearch didn't return the [Content-Type] header, unable to parse response body");
219219
}
220-
XContentType xContentType = XContentType.fromMediaTypeOrFormat(entity.getContentType().getValue());
220+
XContentType xContentType = XContentType.fromMediaType(entity.getContentType().getValue());
221221
if (xContentType == null) {
222222
throw new IllegalStateException("Unsupported Content-Type: " + entity.getContentType().getValue());
223223
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1899,7 +1899,7 @@ protected final <Resp> Resp parseEntity(final HttpEntity entity,
18991899
if (entity.getContentType() == null) {
19001900
throw new IllegalStateException("Elasticsearch didn't return the [Content-Type] header, unable to parse response body");
19011901
}
1902-
XContentType xContentType = XContentType.fromMediaTypeOrFormat(entity.getContentType().getValue());
1902+
XContentType xContentType = XContentType.fromMediaType(entity.getContentType().getValue());
19031903
if (xContentType == null) {
19041904
throw new IllegalStateException("Unsupported Content-Type: " + entity.getContentType().getValue());
19051905
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public class PostDataRequest implements Validatable, ToXContentObject {
4747

4848
public static final ConstructingObjectParser<PostDataRequest, Void> PARSER =
4949
new ConstructingObjectParser<>("post_data_request",
50-
(a) -> new PostDataRequest((String)a[0], XContentType.fromMediaTypeOrFormat((String)a[1]), new byte[0]));
50+
(a) -> new PostDataRequest((String)a[0], XContentType.fromMediaType((String)a[1]), new byte[0]));
5151

5252
static {
5353
PARSER.declareString(ConstructingObjectParser.constructorArg(), Job.ID);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,7 @@ public void testUpdate() throws IOException {
767767

768768
UpdateRequest parsedUpdateRequest = new UpdateRequest();
769769

770-
XContentType entityContentType = XContentType.fromMediaTypeOrFormat(entity.getContentType().getValue());
770+
XContentType entityContentType = XContentType.fromMediaType(entity.getContentType().getValue());
771771
try (XContentParser parser = createParser(entityContentType.xContent(), entity.getContent())) {
772772
parsedUpdateRequest.fromXContent(parser);
773773
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.common.xcontent;
21+
22+
/**
23+
* Abstracts a <a href="http://en.wikipedia.org/wiki/Internet_media_type">Media Type</a> and a format parameter.
24+
* Media types are used as values on Content-Type and Accept headers
25+
* format is an URL parameter, specifies response media type.
26+
*/
27+
public interface MediaType {
28+
/**
29+
* Returns a type part of a MediaType
30+
* i.e. application for application/json
31+
*/
32+
String type();
33+
34+
/**
35+
* Returns a subtype part of a MediaType.
36+
* i.e. json for application/json
37+
*/
38+
String subtype();
39+
40+
/**
41+
* Returns a corresponding format for a MediaType. i.e. json for application/json media type
42+
* Can differ from the MediaType's subtype i.e plain/text has a subtype of text but format is txt
43+
*/
44+
String format();
45+
46+
/**
47+
* returns a string representation of a media type.
48+
*/
49+
default String typeWithSubtype(){
50+
return type() + "/" + subtype();
51+
}
52+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.common.xcontent;
21+
22+
import java.util.HashMap;
23+
import java.util.Locale;
24+
import java.util.Map;
25+
26+
public class MediaTypeParser<T extends MediaType> {
27+
private final Map<String, T> formatToMediaType;
28+
private final Map<String, T> typeWithSubtypeToMediaType;
29+
30+
public MediaTypeParser(T[] acceptedMediaTypes) {
31+
this(acceptedMediaTypes, Map.of());
32+
}
33+
34+
public MediaTypeParser(T[] acceptedMediaTypes, Map<String, T> additionalMediaTypes) {
35+
final int size = acceptedMediaTypes.length + additionalMediaTypes.size();
36+
Map<String, T> formatMap = new HashMap<>(size);
37+
Map<String, T> typeMap = new HashMap<>(size);
38+
for (T mediaType : acceptedMediaTypes) {
39+
typeMap.put(mediaType.typeWithSubtype(), mediaType);
40+
formatMap.put(mediaType.format(), mediaType);
41+
}
42+
for (Map.Entry<String, T> entry : additionalMediaTypes.entrySet()) {
43+
String typeWithSubtype = entry.getKey();
44+
T mediaType = entry.getValue();
45+
46+
typeMap.put(typeWithSubtype.toLowerCase(Locale.ROOT), mediaType);
47+
formatMap.put(mediaType.format(), mediaType);
48+
}
49+
50+
this.formatToMediaType = Map.copyOf(formatMap);
51+
this.typeWithSubtypeToMediaType = Map.copyOf(typeMap);
52+
}
53+
54+
public T fromMediaType(String mediaType) {
55+
ParsedMediaType parsedMediaType = parseMediaType(mediaType);
56+
return parsedMediaType != null ? parsedMediaType.getMediaType() : null;
57+
}
58+
59+
public T fromFormat(String format) {
60+
if (format == null) {
61+
return null;
62+
}
63+
return formatToMediaType.get(format.toLowerCase(Locale.ROOT));
64+
}
65+
66+
/**
67+
* parsing media type that follows https://tools.ietf.org/html/rfc7231#section-3.1.1.1
68+
* @param headerValue a header value from Accept or Content-Type
69+
* @return a parsed media-type
70+
*/
71+
public ParsedMediaType parseMediaType(String headerValue) {
72+
if (headerValue != null) {
73+
String[] split = headerValue.toLowerCase(Locale.ROOT).split(";");
74+
75+
String[] typeSubtype = split[0].trim().toLowerCase(Locale.ROOT)
76+
.split("/");
77+
if (typeSubtype.length == 2) {
78+
String type = typeSubtype[0];
79+
String subtype = typeSubtype[1];
80+
T xContentType = typeWithSubtypeToMediaType.get(type + "/" + subtype);
81+
if (xContentType != null) {
82+
Map<String, String> parameters = new HashMap<>();
83+
for (int i = 1; i < split.length; i++) {
84+
//spaces are allowed between parameters, but not between '=' sign
85+
String[] keyValueParam = split[i].trim().split("=");
86+
if (keyValueParam.length != 2 || hasSpaces(keyValueParam[0]) || hasSpaces(keyValueParam[1])) {
87+
return null;
88+
}
89+
parameters.put(keyValueParam[0].toLowerCase(Locale.ROOT), keyValueParam[1].toLowerCase(Locale.ROOT));
90+
}
91+
return new ParsedMediaType(xContentType, parameters);
92+
}
93+
}
94+
95+
}
96+
return null;
97+
}
98+
99+
private boolean hasSpaces(String s) {
100+
return s.trim().equals(s) == false;
101+
}
102+
103+
/**
104+
* A media type object that contains all the information provided on a Content-Type or Accept header
105+
*/
106+
public class ParsedMediaType {
107+
private final Map<String, String> parameters;
108+
private final T mediaType;
109+
110+
public ParsedMediaType(T mediaType, Map<String, String> parameters) {
111+
this.parameters = parameters;
112+
this.mediaType = mediaType;
113+
}
114+
115+
public T getMediaType() {
116+
return mediaType;
117+
}
118+
119+
public Map<String, String> getParameters() {
120+
return parameters;
121+
}
122+
}
123+
}

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

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,12 @@
2424
import org.elasticsearch.common.xcontent.smile.SmileXContent;
2525
import org.elasticsearch.common.xcontent.yaml.YamlXContent;
2626

27-
import java.util.Locale;
28-
import java.util.Objects;
27+
import java.util.Map;
2928

3029
/**
3130
* The content type of {@link org.elasticsearch.common.xcontent.XContent}.
3231
*/
33-
public enum XContentType {
32+
public enum XContentType implements MediaType {
3433

3534
/**
3635
* A JSON based content type.
@@ -47,7 +46,7 @@ public String mediaType() {
4746
}
4847

4948
@Override
50-
public String shortName() {
49+
public String subtype() {
5150
return "json";
5251
}
5352

@@ -66,7 +65,7 @@ public String mediaTypeWithoutParameters() {
6665
}
6766

6867
@Override
69-
public String shortName() {
68+
public String subtype() {
7069
return "smile";
7170
}
7271

@@ -85,7 +84,7 @@ public String mediaTypeWithoutParameters() {
8584
}
8685

8786
@Override
88-
public String shortName() {
87+
public String subtype() {
8988
return "yaml";
9089
}
9190

@@ -104,7 +103,7 @@ public String mediaTypeWithoutParameters() {
104103
}
105104

106105
@Override
107-
public String shortName() {
106+
public String subtype() {
108107
return "cbor";
109108
}
110109

@@ -114,54 +113,30 @@ public XContent xContent() {
114113
}
115114
};
116115

116+
public static final MediaTypeParser<XContentType> mediaTypeParser = new MediaTypeParser<>(XContentType.values(),
117+
Map.of("application/*", JSON, "application/x-ndjson", JSON));
118+
119+
117120
/**
118-
* Accepts either a format string, which is equivalent to {@link XContentType#shortName()} or a media type that optionally has
119-
* parameters and attempts to match the value to an {@link XContentType}. The comparisons are done in lower case format and this method
120-
* also supports a wildcard accept for {@code application/*}. This method can be used to parse the {@code Accept} HTTP header or a
121-
* format query string parameter. This method will return {@code null} if no match is found
121+
* Accepts a format string, which is most of the time is equivalent to {@link XContentType#subtype()}
122+
* and attempts to match the value to an {@link XContentType}.
123+
* The comparisons are done in lower case format.
124+
* This method will return {@code null} if no match is found
122125
*/
123-
public static XContentType fromMediaTypeOrFormat(String mediaType) {
124-
if (mediaType == null) {
125-
return null;
126-
}
127-
for (XContentType type : values()) {
128-
if (isSameMediaTypeOrFormatAs(mediaType, type)) {
129-
return type;
130-
}
131-
}
132-
final String lowercaseMediaType = mediaType.toLowerCase(Locale.ROOT);
133-
if (lowercaseMediaType.startsWith("application/*")) {
134-
return JSON;
135-
}
136-
137-
return null;
126+
public static XContentType fromFormat(String mediaType) {
127+
return mediaTypeParser.fromFormat(mediaType);
138128
}
139129

140130
/**
141131
* Attempts to match the given media type with the known {@link XContentType} values. This match is done in a case-insensitive manner.
142-
* The provided media type should not include any parameters. This method is suitable for parsing part of the {@code Content-Type}
143-
* HTTP header. This method will return {@code null} if no match is found
132+
* The provided media type can optionally has parameters.
133+
* This method is suitable for parsing of the {@code Content-Type} and {@code Accept} HTTP headers.
134+
* This method will return {@code null} if no match is found
144135
*/
145-
public static XContentType fromMediaType(String mediaType) {
146-
final String lowercaseMediaType = Objects.requireNonNull(mediaType, "mediaType cannot be null").toLowerCase(Locale.ROOT);
147-
for (XContentType type : values()) {
148-
if (type.mediaTypeWithoutParameters().equals(lowercaseMediaType)) {
149-
return type;
150-
}
151-
}
152-
// we also support newline delimited JSON: http://specs.okfnlabs.org/ndjson/
153-
if (lowercaseMediaType.toLowerCase(Locale.ROOT).equals("application/x-ndjson")) {
154-
return XContentType.JSON;
155-
}
156-
157-
return null;
136+
public static XContentType fromMediaType(String mediaTypeHeaderValue) {
137+
return mediaTypeParser.fromMediaType(mediaTypeHeaderValue);
158138
}
159139

160-
private static boolean isSameMediaTypeOrFormatAs(String stringType, XContentType type) {
161-
return type.mediaTypeWithoutParameters().equalsIgnoreCase(stringType) ||
162-
stringType.toLowerCase(Locale.ROOT).startsWith(type.mediaTypeWithoutParameters().toLowerCase(Locale.ROOT) + ";") ||
163-
type.shortName().equalsIgnoreCase(stringType);
164-
}
165140

166141
private int index;
167142

@@ -177,10 +152,19 @@ public String mediaType() {
177152
return mediaTypeWithoutParameters();
178153
}
179154

180-
public abstract String shortName();
181155

182156
public abstract XContent xContent();
183157

184158
public abstract String mediaTypeWithoutParameters();
185159

160+
161+
@Override
162+
public String type() {
163+
return "application";
164+
}
165+
166+
@Override
167+
public String format() {
168+
return subtype();
169+
}
186170
}

0 commit comments

Comments
 (0)