Skip to content

Commit 5e74f79

Browse files
authored
Support response content-type with versioned media type (#65500)
This commit allows returning a correct requested response content-type - it did not work for versioned media types. It is done by adding new vendor specific instances to XContent and TextFormat enums. These instances can then "format" the response content type string when provided with parameters. This is similar to what SQL plugin does with its media types. #51816
1 parent f24665f commit 5e74f79

File tree

61 files changed

+446
-122
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+446
-122
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,14 +1177,14 @@ Params withWaitForEvents(Priority waitForEvents) {
11771177
*/
11781178
static XContentType enforceSameContentType(IndexRequest indexRequest, @Nullable XContentType xContentType) {
11791179
XContentType requestContentType = indexRequest.getContentType();
1180-
if (requestContentType != XContentType.JSON && requestContentType != XContentType.SMILE) {
1180+
if (requestContentType.canonical() != XContentType.JSON && requestContentType.canonical() != XContentType.SMILE) {
11811181
throw new IllegalArgumentException("Unsupported content-type found for request with content-type [" + requestContentType
11821182
+ "], only JSON and SMILE are supported");
11831183
}
11841184
if (xContentType == null) {
11851185
return requestContentType;
11861186
}
1187-
if (requestContentType != xContentType) {
1187+
if (requestContentType.canonical() != xContentType.canonical()) {
11881188
throw new IllegalArgumentException("Mismatching content-type found for request with content-type [" + requestContentType
11891189
+ "], previous requests have content-type [" + xContentType + "]");
11901190
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1821,9 +1821,11 @@ public void testCreateContentType() {
18211821
}
18221822

18231823
public void testEnforceSameContentType() {
1824-
XContentType xContentType = randomFrom(XContentType.JSON, XContentType.SMILE);
1824+
XContentType xContentType = randomFrom(XContentType.JSON, XContentType.SMILE, XContentType.VND_JSON, XContentType.VND_SMILE);
18251825
IndexRequest indexRequest = new IndexRequest().source(singletonMap("field", "value"), xContentType);
1826-
assertEquals(xContentType, enforceSameContentType(indexRequest, null));
1826+
// indexRequest content type is made canonical because IndexRequest's content-type is
1827+
// from XContentBuilder.getXContentType (hardcoded in JsonXContentGEnerator)
1828+
assertEquals(xContentType.canonical(), enforceSameContentType(indexRequest, null));
18271829
assertEquals(xContentType, enforceSameContentType(indexRequest, xContentType));
18281830

18291831
XContentType bulkContentType = randomBoolean() ? xContentType : null;
@@ -1840,7 +1842,7 @@ public void testEnforceSameContentType() {
18401842
assertEquals("Unsupported content-type found for request with content-type [YAML], only JSON and SMILE are supported",
18411843
exception.getMessage());
18421844

1843-
XContentType requestContentType = xContentType == XContentType.JSON ? XContentType.SMILE : XContentType.JSON;
1845+
XContentType requestContentType = xContentType.canonical() == XContentType.JSON ? XContentType.SMILE : XContentType.JSON;
18441846

18451847
exception = expectThrows(IllegalArgumentException.class,
18461848
() -> enforceSameContentType(new IndexRequest().source(singletonMap("field", "value"), requestContentType), xContentType));

docs/reference/search/search-template.asciidoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ The API returns the following result:
158158
"lang" : "mustache",
159159
"source" : """{"query":{"match":{"title":"{{query_string}}"}}}""",
160160
"options": {
161-
"content_type" : "application/json; charset=UTF-8"
161+
"content_type" : "application/json;charset=utf-8"
162162
}
163163
},
164164
"_id": "<templateid>",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@
3333
* I.e. txt used in path _sql?format=txt will return TextFormat.PLAIN_TEXT
3434
*
3535
* Multiple header representations may map to a single {@link MediaType} for example, "application/json"
36-
* and "application/vnd.elasticsearch+json" both represent a JSON MediaType.
36+
* and "application/x-ndjson" both represent a JSON MediaType.
3737
* A MediaType can have only one query parameter representation.
3838
* For example "json" (case insensitive) maps back to a JSON media type.
3939
*
40-
* Additionally, a http header may optionally have parameters. For example "application/json; charset=utf-8".
40+
* Additionally, a http header may optionally have parameters. For example "application/vnd.elasticsearch+json; compatible-with=7".
4141
* This class also allows to define a regular expression for valid values of charset.
4242
*/
4343
public class MediaTypeRegistry<T extends MediaType> {

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Locale;
2525
import java.util.Map;
2626
import java.util.regex.Pattern;
27+
import java.util.stream.Collectors;
2728

2829
/**
2930
* A raw result of parsing media types from Accept or Content-Type headers.
@@ -44,7 +45,7 @@ private ParsedMediaType(String originalHeaderValue, String type, String subType,
4445
this.originalHeaderValue = originalHeaderValue;
4546
this.type = type;
4647
this.subType = subType;
47-
this.parameters = Collections.unmodifiableMap(parameters);
48+
this.parameters = parameters;
4849
}
4950

5051
/**
@@ -55,7 +56,7 @@ public String mediaTypeWithoutParameters() {
5556
}
5657

5758
public Map<String, String> getParameters() {
58-
return parameters;
59+
return Collections.unmodifiableMap(parameters);
5960
}
6061

6162
/**
@@ -81,7 +82,7 @@ public static ParsedMediaType parseMediaType(String headerValue) {
8182
throw new IllegalArgumentException("invalid media-type [" + headerValue + "]");
8283
}
8384
if (elements.length == 1) {
84-
return new ParsedMediaType(headerValue, splitMediaType[0].trim(), splitMediaType[1].trim(), Collections.emptyMap());
85+
return new ParsedMediaType(headerValue, splitMediaType[0].trim(), splitMediaType[1].trim(), new HashMap<>());
8586
} else {
8687
Map<String, String> parameters = new HashMap<>();
8788
for (int i = 1; i < elements.length; i++) {
@@ -105,6 +106,12 @@ public static ParsedMediaType parseMediaType(String headerValue) {
105106
return null;
106107
}
107108

109+
public static ParsedMediaType parseMediaType(XContentType requestContentType, Map<String, String> parameters) {
110+
ParsedMediaType parsedMediaType = parseMediaType(requestContentType.mediaTypeWithoutParameters());
111+
parsedMediaType.parameters.putAll(parameters);
112+
return parsedMediaType;
113+
}
114+
108115
// simplistic check for media ranges. do not validate if this is a correct header
109116
private static boolean isMediaRange(String headerValue) {
110117
return headerValue.contains(",");
@@ -151,4 +158,16 @@ private boolean isValidParameter(String paramName, String value, Map<String, Pat
151158
public String toString() {
152159
return originalHeaderValue;
153160
}
161+
162+
public String responseContentTypeHeader(Map<String,String> parameters) {
163+
return this.mediaTypeWithoutParameters() + formatParameters(parameters);
164+
}
165+
166+
private String formatParameters(Map<String, String> parameters) {
167+
String joined = parameters.entrySet().stream()
168+
.map(e -> e.getKey() + "=" + e.getValue())
169+
.collect(Collectors.joining(";"));
170+
return joined.isEmpty() ? "" : ";" + joined;
171+
}
172+
154173
}

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

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@
4848
*/
4949
public final class XContentBuilder implements Closeable, Flushable {
5050

51-
private byte compatibleMajorVersion;
52-
5351
/**
5452
* Create a new {@link XContentBuilder} using the given {@link XContent} content.
5553
* <p>
@@ -65,20 +63,21 @@ public static XContentBuilder builder(XContent xContent) throws IOException {
6563
}
6664

6765
/**
68-
* Create a new {@link XContentBuilder} using the given {@link XContent} content and some inclusive and/or exclusive filters.
66+
* Create a new {@link XContentBuilder} using the given {@link XContentType} xContentType and some inclusive and/or exclusive filters.
6967
* <p>
7068
* The builder uses an internal {@link ByteArrayOutputStream} output stream to build the content. When both exclusive and
7169
* inclusive filters are provided, the underlying builder will first use exclusion filters to remove fields and then will check the
7270
* remaining fields against the inclusive filters.
7371
* <p>
7472
*
75-
* @param xContent the {@link XContent}
73+
* @param xContentType the {@link XContentType}
7674
* @param includes the inclusive filters: only fields and objects that match the inclusive filters will be written to the output.
7775
* @param excludes the exclusive filters: only fields and objects that don't match the exclusive filters will be written to the output.
7876
* @throws IOException if an {@link IOException} occurs while building the content
7977
*/
80-
public static XContentBuilder builder(XContent xContent, Set<String> includes, Set<String> excludes) throws IOException {
81-
return new XContentBuilder(xContent, new ByteArrayOutputStream(), includes, excludes);
78+
public static XContentBuilder builder(XContentType xContentType, Set<String> includes, Set<String> excludes) throws IOException {
79+
return new XContentBuilder(xContentType.xContent(), new ByteArrayOutputStream(), includes, excludes,
80+
ParsedMediaType.parseMediaType(xContentType.mediaType()));
8281
}
8382

8483
private static final Map<Class<?>, Writer> WRITERS;
@@ -167,12 +166,16 @@ public interface HumanReadableTransformer {
167166
*/
168167
private boolean humanReadable = false;
169168

169+
private byte compatibleMajorVersion;
170+
171+
private ParsedMediaType responseContentType;
172+
170173
/**
171174
* Constructs a new builder using the provided XContent and an OutputStream. Make sure
172175
* to call {@link #close()} when the builder is done with.
173176
*/
174177
public XContentBuilder(XContent xContent, OutputStream bos) throws IOException {
175-
this(xContent, bos, Collections.emptySet(), Collections.emptySet());
178+
this(xContent, bos, Collections.emptySet(), Collections.emptySet(), ParsedMediaType.parseMediaType(xContent.type().mediaType()));
176179
}
177180

178181
/**
@@ -181,8 +184,8 @@ public XContentBuilder(XContent xContent, OutputStream bos) throws IOException {
181184
* filter will be written to the output stream. Make sure to call
182185
* {@link #close()} when the builder is done with.
183186
*/
184-
public XContentBuilder(XContent xContent, OutputStream bos, Set<String> includes) throws IOException {
185-
this(xContent, bos, includes, Collections.emptySet());
187+
public XContentBuilder(XContentType xContentType, OutputStream bos, Set<String> includes) throws IOException {
188+
this(xContentType.xContent(), bos, includes, Collections.emptySet(), ParsedMediaType.parseMediaType(xContentType.mediaType()));
186189
}
187190

188191
/**
@@ -191,16 +194,25 @@ public XContentBuilder(XContent xContent, OutputStream bos, Set<String> includes
191194
* remaining fields against the inclusive filters.
192195
* <p>
193196
* Make sure to call {@link #close()} when the builder is done with.
194-
*
195197
* @param os the output stream
196198
* @param includes the inclusive filters: only fields and objects that match the inclusive filters will be written to the output.
197199
* @param excludes the exclusive filters: only fields and objects that don't match the exclusive filters will be written to the output.
200+
* @param responseContentType a content-type header value to be send back on a response
198201
*/
199-
public XContentBuilder(XContent xContent, OutputStream os, Set<String> includes, Set<String> excludes) throws IOException {
202+
public XContentBuilder(XContent xContent, OutputStream os, Set<String> includes, Set<String> excludes,
203+
ParsedMediaType responseContentType) throws IOException {
200204
this.bos = os;
205+
assert responseContentType != null : "generated response cannot be null";
206+
this.responseContentType = responseContentType;
201207
this.generator = xContent.createGenerator(bos, includes, excludes);
202208
}
203209

210+
public String getResponseContentTypeString() {
211+
Map<String, String> parameters = responseContentType != null ?
212+
responseContentType.getParameters() : Collections.emptyMap();
213+
return responseContentType.responseContentTypeHeader(parameters);
214+
}
215+
204216
public XContentType contentType() {
205217
return generator.contentType();
206218
}

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,14 @@ public static XContentBuilder cborBuilder(OutputStream os) throws IOException {
9797
* Constructs a xcontent builder that will output the result into the provided output stream.
9898
*/
9999
public static XContentBuilder contentBuilder(XContentType type, OutputStream outputStream) throws IOException {
100-
if (type == XContentType.JSON) {
100+
XContentType canonical = type.canonical();
101+
if (canonical == XContentType.JSON) {
101102
return jsonBuilder(outputStream);
102-
} else if (type == XContentType.SMILE) {
103+
} else if (canonical == XContentType.SMILE) {
103104
return smileBuilder(outputStream);
104-
} else if (type == XContentType.YAML) {
105+
} else if (canonical == XContentType.YAML) {
105106
return yamlBuilder(outputStream);
106-
} else if (type == XContentType.CBOR) {
107+
} else if (canonical == XContentType.CBOR) {
107108
return cborBuilder(outputStream);
108109
}
109110
throw new IllegalArgumentException("No matching content type for " + type);
@@ -113,13 +114,15 @@ public static XContentBuilder contentBuilder(XContentType type, OutputStream out
113114
* Returns a binary content builder for the provided content type.
114115
*/
115116
public static XContentBuilder contentBuilder(XContentType type) throws IOException {
116-
if (type == XContentType.JSON) {
117+
XContentType canonical = type.canonical();
118+
119+
if (canonical == XContentType.JSON) {
117120
return JsonXContent.contentBuilder();
118-
} else if (type == XContentType.SMILE) {
121+
} else if (canonical == XContentType.SMILE) {
119122
return SmileXContent.contentBuilder();
120-
} else if (type == XContentType.YAML) {
123+
} else if (canonical == XContentType.YAML) {
121124
return YamlXContent.contentBuilder();
122-
} else if (type == XContentType.CBOR) {
125+
} else if (canonical == XContentType.CBOR) {
123126
return CborXContent.contentBuilder();
124127
}
125128
throw new IllegalArgumentException("No matching content type for " + type);

0 commit comments

Comments
 (0)