Skip to content

Commit f4c9f6b

Browse files
committed
Media types by Class for HttpMessageConverter
See gh-26212
1 parent 1721b0b commit f4c9f6b

File tree

13 files changed

+164
-86
lines changed

13 files changed

+164
-86
lines changed

spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.http.converter;
1818

1919
import java.io.IOException;
20+
import java.util.Collections;
2021
import java.util.List;
2122

2223
import org.springframework.http.HttpInputMessage;
@@ -53,11 +54,30 @@ public interface HttpMessageConverter<T> {
5354
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
5455

5556
/**
56-
* Return the list of {@link MediaType} objects supported by this converter.
57-
* @return the list of supported media types, potentially an immutable copy
57+
* Return the list of media types supported by this converter. The list may
58+
* not apply to every possible target element type and calls to this method
59+
* should typically be guarded via {@link #canWrite(Class, MediaType)
60+
* canWrite(clazz, null}. The list may also exclude MIME types supported
61+
* only for a specific class. Alternatively, use
62+
* {@link #getSupportedMediaTypes(Class)} for a more precise list.
63+
* @return the list of supported media types
5864
*/
5965
List<MediaType> getSupportedMediaTypes();
6066

67+
/**
68+
* Return the list of media types supported by this converter for the given
69+
* class. The list may differ from {@link #getSupportedMediaTypes()} if the
70+
* converter doesn't support given Class or if it support it only for a
71+
* subset of media types.
72+
* @param clazz the type of class to check
73+
* @return the list of media types supported for the given class
74+
* @since 5.3.4
75+
*/
76+
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
77+
return (canRead(clazz, null) || canWrite(clazz, null) ?
78+
getSupportedMediaTypes() : Collections.emptyList());
79+
}
80+
6181
/**
6282
* Read an object of the given type from the given input message, and returns it.
6383
* @param clazz the type of object to return. This type must have previously been passed to the

spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java

+13
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.lang.reflect.Type;
2424
import java.nio.charset.Charset;
2525
import java.nio.charset.StandardCharsets;
26+
import java.util.ArrayList;
2627
import java.util.Arrays;
2728
import java.util.Collections;
2829
import java.util.LinkedHashMap;
@@ -194,6 +195,18 @@ public Map<MediaType, ObjectMapper> getObjectMappersForType(Class<?> clazz) {
194195
return Collections.emptyMap();
195196
}
196197

198+
@Override
199+
public List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
200+
List<MediaType> result = null;
201+
for (Map.Entry<Class<?>, Map<MediaType, ObjectMapper>> entry : getObjectMapperRegistrations().entrySet()) {
202+
if (entry.getKey().isAssignableFrom(clazz)) {
203+
result = (result != null ? result : new ArrayList<>(entry.getValue().size()));
204+
result.addAll(entry.getValue().keySet());
205+
}
206+
}
207+
return (CollectionUtils.isEmpty(result) ? getSupportedMediaTypes() : result);
208+
}
209+
197210
private Map<Class<?>, Map<MediaType, ObjectMapper>> getObjectMapperRegistrations() {
198211
return (this.objectMapperRegistrations != null ? this.objectMapperRegistrations : Collections.emptyMap());
199212
}

spring-web/src/main/java/org/springframework/web/client/RestTemplate.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.web.client;
1818

1919
import java.io.IOException;
20+
import java.lang.reflect.ParameterizedType;
2021
import java.lang.reflect.Type;
2122
import java.net.URI;
2223
import java.util.ArrayList;
@@ -885,7 +886,7 @@ public void doWithRequest(ClientHttpRequest request) throws IOException {
885886
if (this.responseType != null) {
886887
List<MediaType> allSupportedMediaTypes = getMessageConverters().stream()
887888
.filter(converter -> canReadResponse(this.responseType, converter))
888-
.flatMap(this::getSupportedMediaTypes)
889+
.flatMap((HttpMessageConverter<?> converter) -> getSupportedMediaTypes(this.responseType, converter))
889890
.distinct()
890891
.sorted(MediaType.SPECIFICITY_COMPARATOR)
891892
.collect(Collectors.toList());
@@ -908,8 +909,10 @@ else if (converter instanceof GenericHttpMessageConverter) {
908909
return false;
909910
}
910911

911-
private Stream<MediaType> getSupportedMediaTypes(HttpMessageConverter<?> messageConverter) {
912-
return messageConverter.getSupportedMediaTypes()
912+
private Stream<MediaType> getSupportedMediaTypes(Type type, HttpMessageConverter<?> converter) {
913+
Type rawType = (type instanceof ParameterizedType ? ((ParameterizedType) type).getRawType() : type);
914+
Class<?> clazz = (rawType instanceof Class ? (Class<?>) rawType : null);
915+
return (clazz != null ? converter.getSupportedMediaTypes(clazz) : converter.getSupportedMediaTypes())
913916
.stream()
914917
.map(mediaType -> {
915918
if (mediaType.getCharset() != null) {

spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java

+16
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ public void canReadAndWriteMicroformats() {
109109
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "vnd.test-micro-type+json"))).isTrue();
110110
}
111111

112+
@Test
113+
public void getSupportedMediaTypes() {
114+
MediaType[] defaultMediaTypes = {MediaType.APPLICATION_JSON, MediaType.parseMediaType("application/*+json")};
115+
assertThat(converter.getSupportedMediaTypes()).containsExactly(defaultMediaTypes);
116+
assertThat(converter.getSupportedMediaTypes(MyBean.class)).containsExactly(defaultMediaTypes);
117+
118+
MediaType halJson = MediaType.parseMediaType("application/hal+json");
119+
converter.registerObjectMappersForType(MyBean.class, map -> {
120+
map.put(halJson, new ObjectMapper());
121+
map.put(MediaType.APPLICATION_JSON, new ObjectMapper());
122+
});
123+
124+
assertThat(converter.getSupportedMediaTypes(MyBean.class)).containsExactly(halJson, MediaType.APPLICATION_JSON);
125+
assertThat(converter.getSupportedMediaTypes(Map.class)).containsExactly(defaultMediaTypes);
126+
}
127+
112128
@Test
113129
public void readTyped() throws IOException {
114130
String body = "{" +

spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -232,12 +232,10 @@ void getUnsupportedMediaType() throws Exception {
232232
void requestAvoidsDuplicateAcceptHeaderValues() throws Exception {
233233
HttpMessageConverter<?> firstConverter = mock(HttpMessageConverter.class);
234234
given(firstConverter.canRead(any(), any())).willReturn(true);
235-
given(firstConverter.getSupportedMediaTypes())
236-
.willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
235+
given(firstConverter.getSupportedMediaTypes(any())).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
237236
HttpMessageConverter<?> secondConverter = mock(HttpMessageConverter.class);
238237
given(secondConverter.canRead(any(), any())).willReturn(true);
239-
given(secondConverter.getSupportedMediaTypes())
240-
.willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
238+
given(secondConverter.getSupportedMediaTypes(any())).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
241239

242240
HttpHeaders requestHeaders = new HttpHeaders();
243241
mockSentRequest(GET, "https://example.com/", requestHeaders);
@@ -651,7 +649,7 @@ void exchangeParameterizedType() throws Exception {
651649
template.setMessageConverters(Collections.<HttpMessageConverter<?>>singletonList(converter));
652650
ParameterizedTypeReference<List<Integer>> intList = new ParameterizedTypeReference<List<Integer>>() {};
653651
given(converter.canRead(intList.getType(), null, null)).willReturn(true);
654-
given(converter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
652+
given(converter.getSupportedMediaTypes(any())).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
655653
given(converter.canWrite(String.class, String.class, null)).willReturn(true);
656654

657655
HttpHeaders requestHeaders = new HttpHeaders();
@@ -774,8 +772,7 @@ private void mockTextPlainHttpMessageConverter() {
774772
private void mockHttpMessageConverter(MediaType mediaType, Class<?> type) {
775773
given(converter.canRead(type, null)).willReturn(true);
776774
given(converter.canRead(type, mediaType)).willReturn(true);
777-
given(converter.getSupportedMediaTypes())
778-
.willReturn(Collections.singletonList(mediaType));
775+
given(converter.getSupportedMediaTypes(type)).willReturn(Collections.singletonList(mediaType));
779776
given(converter.canRead(type, mediaType)).willReturn(true);
780777
given(converter.canWrite(type, null)).willReturn(true);
781778
given(converter.canWrite(type, mediaType)).willReturn(true);

spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -339,7 +339,7 @@ private static List<MediaType> producibleMediaTypes(
339339

340340
return messageConverters.stream()
341341
.filter(messageConverter -> messageConverter.canWrite(entityClass, null))
342-
.flatMap(messageConverter -> messageConverter.getSupportedMediaTypes().stream())
342+
.flatMap(messageConverter -> messageConverter.getSupportedMediaTypes(entityClass).stream())
343343
.collect(Collectors.toList());
344344
}
345345

spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java

+9-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -81,8 +81,6 @@ class DefaultServerRequest implements ServerRequest {
8181

8282
private final List<HttpMessageConverter<?>> messageConverters;
8383

84-
private final List<MediaType> allSupportedMediaTypes;
85-
8684
private final MultiValueMap<String, String> params;
8785

8886
private final Map<String, Object> attributes;
@@ -94,7 +92,6 @@ class DefaultServerRequest implements ServerRequest {
9492
public DefaultServerRequest(HttpServletRequest servletRequest, List<HttpMessageConverter<?>> messageConverters) {
9593
this.serverHttpRequest = new ServletServerHttpRequest(servletRequest);
9694
this.messageConverters = Collections.unmodifiableList(new ArrayList<>(messageConverters));
97-
this.allSupportedMediaTypes = allSupportedMediaTypes(messageConverters);
9895

9996
this.headers = new DefaultRequestHeaders(this.serverHttpRequest.getHeaders());
10097
this.params = CollectionUtils.toMultiValueMap(new ServletParametersMap(servletRequest));
@@ -107,13 +104,6 @@ public DefaultServerRequest(HttpServletRequest servletRequest, List<HttpMessageC
107104
ServletRequestPathUtils.parseAndCache(servletRequest));
108105
}
109106

110-
private static List<MediaType> allSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) {
111-
return messageConverters.stream()
112-
.flatMap(converter -> converter.getSupportedMediaTypes().stream())
113-
.sorted(MediaType.SPECIFICITY_COMPARATOR)
114-
.collect(Collectors.toList());
115-
}
116-
117107

118108
@Override
119109
public String methodName() {
@@ -211,7 +201,14 @@ private <T> T bodyInternal(Type bodyType, Class<?> bodyClass) throws ServletExce
211201
return theConverter.read(clazz, this.serverHttpRequest);
212202
}
213203
}
214-
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
204+
throw new HttpMediaTypeNotSupportedException(contentType, getSupportedMediaTypes(bodyClass));
205+
}
206+
207+
private List<MediaType> getSupportedMediaTypes(Class<?> bodyClass) {
208+
return this.messageConverters.stream()
209+
.flatMap(converter -> converter.getSupportedMediaTypes(bodyClass).stream())
210+
.sorted(MediaType.SPECIFICITY_COMPARATOR)
211+
.collect(Collectors.toList());
215212
}
216213

217214
@Override

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java

+17-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@
2323
import java.lang.reflect.Type;
2424
import java.util.ArrayList;
2525
import java.util.Collection;
26-
import java.util.Collections;
2726
import java.util.EnumSet;
2827
import java.util.LinkedHashSet;
2928
import java.util.List;
@@ -80,8 +79,6 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
8079

8180
protected final List<HttpMessageConverter<?>> messageConverters;
8281

83-
protected final List<MediaType> allSupportedMediaTypes;
84-
8582
private final RequestResponseBodyAdviceChain advice;
8683

8784

@@ -101,26 +98,10 @@ public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<
10198

10299
Assert.notEmpty(converters, "'messageConverters' must not be empty");
103100
this.messageConverters = converters;
104-
this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters);
105101
this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice);
106102
}
107103

108104

109-
/**
110-
* Return the media types supported by all provided message converters sorted
111-
* by specificity via {@link MediaType#sortBySpecificity(List)}.
112-
*/
113-
private static List<MediaType> getAllSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) {
114-
Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<>();
115-
for (HttpMessageConverter<?> messageConverter : messageConverters) {
116-
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
117-
}
118-
List<MediaType> result = new ArrayList<>(allSupportedMediaTypes);
119-
MediaType.sortBySpecificity(result);
120-
return Collections.unmodifiableList(result);
121-
}
122-
123-
124105
/**
125106
* Return the configured {@link RequestBodyAdvice} and
126107
* {@link RequestBodyAdvice} where each instance may be wrapped as a
@@ -222,7 +203,7 @@ protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, Me
222203
(noContentType && !message.hasBody())) {
223204
return null;
224205
}
225-
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
206+
throw new HttpMediaTypeNotSupportedException(contentType, getSupportedMediaTypes(targetClass));
226207
}
227208

228209
MediaType selectedContentType = contentType;
@@ -283,6 +264,21 @@ protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter
283264
return !hasBindingResult;
284265
}
285266

267+
/**
268+
* Return the media types supported by all provided message converters sorted
269+
* by specificity via {@link MediaType#sortBySpecificity(List)}.
270+
* @since 5.3.4
271+
*/
272+
protected List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
273+
Set<MediaType> mediaTypeSet = new LinkedHashSet<>();
274+
for (HttpMessageConverter<?> converter : this.messageConverters) {
275+
mediaTypeSet.addAll(converter.getSupportedMediaTypes(clazz));
276+
}
277+
List<MediaType> result = new ArrayList<>(mediaTypeSet);
278+
MediaType.sortBySpecificity(result);
279+
return result;
280+
}
281+
286282
/**
287283
* Adapt the given argument against the method parameter, if necessary.
288284
* @param arg the resolved argument

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java

+10-15
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
299299
throw new HttpMessageNotWritableException(
300300
"No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");
301301
}
302-
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
302+
throw new HttpMediaTypeNotAcceptableException(getSupportedMediaTypes(body.getClass()));
303303
}
304304
}
305305

@@ -361,23 +361,18 @@ protected List<MediaType> getProducibleMediaTypes(
361361
if (!CollectionUtils.isEmpty(mediaTypes)) {
362362
return new ArrayList<>(mediaTypes);
363363
}
364-
else if (!this.allSupportedMediaTypes.isEmpty()) {
365-
List<MediaType> result = new ArrayList<>();
366-
for (HttpMessageConverter<?> converter : this.messageConverters) {
367-
if (converter instanceof GenericHttpMessageConverter && targetType != null) {
368-
if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
369-
result.addAll(converter.getSupportedMediaTypes());
370-
}
371-
}
372-
else if (converter.canWrite(valueClass, null)) {
373-
result.addAll(converter.getSupportedMediaTypes());
364+
List<MediaType> result = new ArrayList<>();
365+
for (HttpMessageConverter<?> converter : this.messageConverters) {
366+
if (converter instanceof GenericHttpMessageConverter && targetType != null) {
367+
if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
368+
result.addAll(converter.getSupportedMediaTypes(valueClass));
374369
}
375370
}
376-
return result;
377-
}
378-
else {
379-
return Collections.singletonList(MediaType.ALL);
371+
else if (converter.canWrite(valueClass, null)) {
372+
result.addAll(converter.getSupportedMediaTypes(valueClass));
373+
}
380374
}
375+
return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result);
381376
}
382377

383378
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)

0 commit comments

Comments
 (0)