Fix issue with @RequestBody args that are type vars

The change to support generic @RequestBody arguments introduced in
3.2 M2 also introduced a regression in reading arguments that are
type variables. This change fixes the issue.

Issue: SPR-9964
This commit is contained in:
Rossen Stoyanchev 2012-11-21 10:40:28 -05:00
parent 4181397c31
commit b7f7fae78a
14 changed files with 202 additions and 105 deletions

View File

@ -27,6 +27,7 @@ import org.springframework.http.MediaType;
* request into a target object of a specified generic type. * request into a target object of a specified generic type.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
* *
* @see org.springframework.core.ParameterizedTypeReference * @see org.springframework.core.ParameterizedTypeReference
@ -36,11 +37,13 @@ public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T>
/** /**
* Indicates whether the given type can be read by this converter. * Indicates whether the given type can be read by this converter.
* @param type the type to test for readability * @param type the type to test for readability
* @param contextClass a context class for the target type, for example a class
* in which the target type appears in a method signature, can be {@code null}
* @param mediaType the media type to read, can be {@code null} if not specified. * @param mediaType the media type to read, can be {@code null} if not specified.
* Typically the value of a {@code Content-Type} header. * Typically the value of a {@code Content-Type} header.
* @return {@code true} if readable; {@code false} otherwise * @return {@code true} if readable; {@code false} otherwise
*/ */
boolean canRead(Type type, MediaType mediaType); boolean canRead(Type type, Class<?> contextClass, MediaType mediaType);
/** /**
* Read an object of the given type form the given input message, and returns it. * Read an object of the given type form the given input message, and returns it.
@ -48,12 +51,14 @@ public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T>
* been passed to the {@link #canRead canRead} method of this interface, * been passed to the {@link #canRead canRead} method of this interface,
* which must have returned {@code true}. * which must have returned {@code true}.
* @param type the type of the target object * @param type the type of the target object
* @param contextClass a context class for the target type, for example a class
* in which the target type appears in a method signature, can be {@code null}
* @param inputMessage the HTTP input message to read from * @param inputMessage the HTTP input message to read from
* @return the converted object * @return the converted object
* @throws IOException in case of I/O errors * @throws IOException in case of I/O errors
* @throws HttpMessageNotReadableException in case of conversion errors * @throws HttpMessageNotReadableException in case of conversion errors
*/ */
T read(Type type, HttpInputMessage inputMessage) T read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException; throws IOException, HttpMessageNotReadableException;
} }

View File

@ -128,11 +128,11 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
@Override @Override
public boolean canRead(Class<?> clazz, MediaType mediaType) { public boolean canRead(Class<?> clazz, MediaType mediaType) {
return canRead((Type) clazz, mediaType); return canRead((Type) clazz, null, mediaType);
} }
public boolean canRead(Type type, MediaType mediaType) { public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
JavaType javaType = getJavaType(type); JavaType javaType = getJavaType(type, contextClass);
return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType)); return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType));
} }
@ -151,14 +151,14 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException { throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(clazz); JavaType javaType = getJavaType(clazz, null);
return readJavaType(javaType, inputMessage); return readJavaType(javaType, inputMessage);
} }
public Object read(Type type, HttpInputMessage inputMessage) public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException { throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(type); JavaType javaType = getJavaType(type, contextClass);
return readJavaType(javaType, inputMessage); return readJavaType(javaType, inputMessage);
} }
@ -197,10 +197,10 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
} }
} }
/** /**
* Return the Jackson {@link JavaType} for the specified type. * Return the Jackson {@link JavaType} for the specified type and context class.
* <p>The default implementation returns {@link ObjectMapper#constructType(java.lang.reflect.Type)}, * <p>The default implementation returns {@link ObjectMapper#constructType(java.lang.reflect.Type)}
* or {@code ObjectMapper.getTypeFactory().constructType(type, contextClass)},
* but this can be overridden in subclasses, to allow for custom generic collection handling. * but this can be overridden in subclasses, to allow for custom generic collection handling.
* For instance: * For instance:
* <pre class="code"> * <pre class="code">
@ -213,10 +213,15 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
* } * }
* </pre> * </pre>
* @param type the type to return the java type for * @param type the type to return the java type for
* @param contextClass a context class for the target type, for example a class
* in which the target type appears in a method signature, can be {@code null}
* signature, can be {@code null}
* @return the java type * @return the java type
*/ */
protected JavaType getJavaType(Type type) { protected JavaType getJavaType(Type type, Class<?> contextClass) {
return this.objectMapper.constructType(type); return (contextClass != null) ?
this.objectMapper.getTypeFactory().constructType(type, contextClass) :
this.objectMapper.constructType(type);
} }
/** /**

View File

@ -128,11 +128,11 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
@Override @Override
public boolean canRead(Class<?> clazz, MediaType mediaType) { public boolean canRead(Class<?> clazz, MediaType mediaType) {
return canRead((Type) clazz, mediaType); return canRead((Type) clazz, null, mediaType);
} }
public boolean canRead(Type type, MediaType mediaType) { public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
JavaType javaType = getJavaType(type); JavaType javaType = getJavaType(type, contextClass);
return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType)); return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType));
} }
@ -151,14 +151,14 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException { throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(clazz); JavaType javaType = getJavaType(clazz, null);
return readJavaType(javaType, inputMessage); return readJavaType(javaType, inputMessage);
} }
public Object read(Type type, HttpInputMessage inputMessage) public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException { throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(type); JavaType javaType = getJavaType(type, contextClass);
return readJavaType(javaType, inputMessage); return readJavaType(javaType, inputMessage);
} }
@ -196,10 +196,10 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
} }
} }
/** /**
* Return the Jackson {@link JavaType} for the specified type. * Return the Jackson {@link JavaType} for the specified type and context class.
* <p>The default implementation returns {@link TypeFactory#type(java.lang.reflect.Type)}, * <p>The default implementation returns {@link TypeFactory#type(java.lang.reflect.Type)}
* or {@code TypeFactory.type(type, TypeFactory.type(contextClass))},
* but this can be overridden in subclasses, to allow for custom generic collection handling. * but this can be overridden in subclasses, to allow for custom generic collection handling.
* For instance: * For instance:
* <pre class="code"> * <pre class="code">
@ -212,10 +212,14 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
* } * }
* </pre> * </pre>
* @param type the type to return the java type for * @param type the type to return the java type for
* @param contextClass a context class for the target type, for example a class
* in which the target type appears in a method signature, can be {@code null}
* @return the java type * @return the java type
*/ */
protected JavaType getJavaType(Type type) { protected JavaType getJavaType(Type type, Class<?> contextClass) {
return TypeFactory.type(type); return (contextClass != null) ?
TypeFactory.type(type, TypeFactory.type(contextClass)) :
TypeFactory.type(type);
} }
/** /**

View File

@ -74,7 +74,7 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
* {@link Collection} where the generic type is a JAXB type annotated with * {@link Collection} where the generic type is a JAXB type annotated with
* {@link XmlRootElement} or {@link XmlType}. * {@link XmlRootElement} or {@link XmlType}.
*/ */
public boolean canRead(Type type, MediaType mediaType) { public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
if (!(type instanceof ParameterizedType)) { if (!(type instanceof ParameterizedType)) {
return false; return false;
} }
@ -119,7 +119,9 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
public T read(Type type, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { public T read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
ParameterizedType parameterizedType = (ParameterizedType) type; ParameterizedType parameterizedType = (ParameterizedType) type;
T result = createCollection((Class<?>) parameterizedType.getRawType()); T result = createCollection((Class<?>) parameterizedType.getRawType());
Class<?> elementClass = (Class<?>) parameterizedType.getActualTypeArguments()[0]; Class<?> elementClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];

View File

@ -86,12 +86,12 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
for (HttpMessageConverter messageConverter : this.messageConverters) { for (HttpMessageConverter messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) { if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter; GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter;
if (genericMessageConverter.canRead(this.responseType, contentType)) { if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Reading [" + this.responseType + "] as \"" + logger.debug("Reading [" + this.responseType + "] as \"" +
contentType + "\" using [" + messageConverter + "]"); contentType + "\" using [" + messageConverter + "]");
} }
return (T) genericMessageConverter.read(this.responseType, response); return (T) genericMessageConverter.read(this.responseType, null, response);
} }
} }
if (this.responseClass != null) { if (this.responseClass != null) {

View File

@ -553,20 +553,17 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
} }
List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>(); List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
for (HttpMessageConverter<?> messageConverter : getMessageConverters()) { for (HttpMessageConverter<?> converter : getMessageConverters()) {
if (responseClass != null) { if (responseClass != null) {
if (messageConverter.canRead(responseClass, null)) { if (converter.canRead(responseClass, null)) {
allSupportedMediaTypes allSupportedMediaTypes.addAll(getSupportedMediaTypes(converter));
.addAll(getSupportedMediaTypes(messageConverter));
} }
} }
else if (messageConverter instanceof GenericHttpMessageConverter) { else if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter genericMessageConverter = GenericHttpMessageConverter genericConverter = (GenericHttpMessageConverter) converter;
(GenericHttpMessageConverter) messageConverter; if (genericConverter.canRead(responseType, null, null)) {
if (genericMessageConverter.canRead(responseType, null)) { allSupportedMediaTypes.addAll(getSupportedMediaTypes(converter));
allSupportedMediaTypes
.addAll(getSupportedMediaTypes(messageConverter));
} }
} }

View File

@ -51,12 +51,12 @@ public class MappingJackson2HttpMessageConverterTests extends AbstractMappingJac
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter() {
@Override @Override
protected JavaType getJavaType(Type type) { protected JavaType getJavaType(Type type, Class<?> contextClass) {
if (type instanceof Class && List.class.isAssignableFrom((Class<?>)type)) { if (type instanceof Class && List.class.isAssignableFrom((Class<?>)type)) {
return new ObjectMapper().getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class); return new ObjectMapper().getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class);
} }
else { else {
return super.getJavaType(type); return super.getJavaType(type, contextClass);
} }
} }
}; };
@ -87,7 +87,7 @@ public class MappingJackson2HttpMessageConverterTests extends AbstractMappingJac
inputMessage.getHeaders().setContentType(new MediaType("application", "json")); inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), inputMessage); List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), null, inputMessage);
assertEquals(1, results.size()); assertEquals(1, results.size());
MyBean result = results.get(0); MyBean result = results.get(0);
assertEquals("Foo", result.getString()); assertEquals("Foo", result.getString());

View File

@ -49,12 +49,12 @@ public class MappingJacksonHttpMessageConverterTests extends AbstractMappingJack
public void readGenerics() throws IOException { public void readGenerics() throws IOException {
MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter() { MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter() {
@Override @Override
protected JavaType getJavaType(Type type) { protected JavaType getJavaType(Type type, Class<?> contextClass) {
if (type instanceof Class && List.class.isAssignableFrom((Class<?>)type)) { if (type instanceof Class && List.class.isAssignableFrom((Class<?>)type)) {
return TypeFactory.collectionType(ArrayList.class, MyBean.class); return TypeFactory.collectionType(ArrayList.class, MyBean.class);
} }
else { else {
return super.getJavaType(type); return super.getJavaType(type, contextClass);
} }
} }
}; };
@ -85,7 +85,7 @@ public class MappingJacksonHttpMessageConverterTests extends AbstractMappingJack
inputMessage.getHeaders().setContentType(new MediaType("application", "json")); inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter(); MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter();
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), inputMessage); List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), null, inputMessage);
assertEquals(1, results.size()); assertEquals(1, results.size());
MyBean result = results.get(0); MyBean result = results.get(0);
assertEquals("Foo", result.getString()); assertEquals("Foo", result.getString());

View File

@ -63,9 +63,9 @@ public class Jaxb2CollectionHttpMessageConverterTests {
@Test @Test
public void canRead() throws Exception { public void canRead() throws Exception {
assertTrue(converter.canRead(rootElementListType, null)); assertTrue(converter.canRead(rootElementListType, null, null));
assertTrue(converter.canRead(rootElementSetType, null)); assertTrue(converter.canRead(rootElementSetType, null, null));
assertTrue(converter.canRead(typeSetType, null)); assertTrue(converter.canRead(typeSetType, null, null));
} }
@Test @Test
@ -74,7 +74,7 @@ public class Jaxb2CollectionHttpMessageConverterTests {
String content = "<list><rootElement><type s=\"1\"/></rootElement><rootElement><type s=\"2\"/></rootElement></list>"; String content = "<list><rootElement><type s=\"1\"/></rootElement><rootElement><type s=\"2\"/></rootElement></list>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
List<RootElement> result = (List<RootElement>) converter.read(rootElementListType, inputMessage); List<RootElement> result = (List<RootElement>) converter.read(rootElementListType, null, inputMessage);
assertEquals("Invalid result", 2, result.size()); assertEquals("Invalid result", 2, result.size());
assertEquals("Invalid result", "1", result.get(0).type.s); assertEquals("Invalid result", "1", result.get(0).type.s);
@ -87,7 +87,7 @@ public class Jaxb2CollectionHttpMessageConverterTests {
String content = "<set><rootElement><type s=\"1\"/></rootElement><rootElement><type s=\"2\"/></rootElement></set>"; String content = "<set><rootElement><type s=\"1\"/></rootElement><rootElement><type s=\"2\"/></rootElement></set>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
Set<RootElement> result = (Set<RootElement>) converter.read(rootElementSetType, inputMessage); Set<RootElement> result = (Set<RootElement>) converter.read(rootElementSetType, null, inputMessage);
assertEquals("Invalid result", 2, result.size()); assertEquals("Invalid result", 2, result.size());
assertTrue("Invalid result", result.contains(new RootElement("1"))); assertTrue("Invalid result", result.contains(new RootElement("1")));
@ -100,7 +100,7 @@ public class Jaxb2CollectionHttpMessageConverterTests {
String content = "<list><foo s=\"1\"/><bar s=\"2\"/></list>"; String content = "<list><foo s=\"1\"/><bar s=\"2\"/></list>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
List<TestType> result = (List<TestType>) converter.read(typeListType, inputMessage); List<TestType> result = (List<TestType>) converter.read(typeListType, null, inputMessage);
assertEquals("Invalid result", 2, result.size()); assertEquals("Invalid result", 2, result.size());
assertEquals("Invalid result", "1", result.get(0).s); assertEquals("Invalid result", "1", result.get(0).s);
@ -113,7 +113,7 @@ public class Jaxb2CollectionHttpMessageConverterTests {
String content = "<set><foo s=\"1\"/><bar s=\"2\"/></set>"; String content = "<set><foo s=\"1\"/><bar s=\"2\"/></set>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
Set<TestType> result = (Set<TestType>) converter.read(typeSetType, inputMessage); Set<TestType> result = (Set<TestType>) converter.read(typeSetType, null, inputMessage);
assertEquals("Invalid result", 2, result.size()); assertEquals("Invalid result", 2, result.size());
assertTrue("Invalid result", result.contains(new TestType("1"))); assertTrue("Invalid result", result.contains(new TestType("1")));

View File

@ -176,8 +176,8 @@ public class HttpMessageConverterExtractorTests {
expect(response.getStatusCode()).andReturn(HttpStatus.OK); expect(response.getStatusCode()).andReturn(HttpStatus.OK);
expect(response.getHeaders()).andReturn(responseHeaders).times(2); expect(response.getHeaders()).andReturn(responseHeaders).times(2);
expect(converter.canRead(type, contentType)).andReturn(true); expect(converter.canRead(type, null, contentType)).andReturn(true);
expect(converter.read(type, response)).andReturn(expected); expect(converter.read(type, null, response)).andReturn(expected);
replay(response, converter); replay(response, converter);
Object result = extractor.extractData(response); Object result = extractor.extractData(response);

View File

@ -86,7 +86,7 @@ public class RestTemplateTests {
verifyMocks(); verifyMocks();
} }
@Test @Test
public void varArgsNullTemplateVariable() throws Exception { public void varArgsNullTemplateVariable() throws Exception {
expect(requestFactory.createRequest(new URI("http://example.com/-foo"), HttpMethod.GET)) expect(requestFactory.createRequest(new URI("http://example.com/-foo"), HttpMethod.GET))
@ -481,7 +481,7 @@ public class RestTemplateTests {
verifyMocks(); verifyMocks();
} }
@Test @Test
public void postForEntityNull() throws Exception { public void postForEntityNull() throws Exception {
MediaType textPlain = new MediaType("text", "plain"); MediaType textPlain = new MediaType("text", "plain");
@ -644,7 +644,7 @@ public class RestTemplateTests {
template.setMessageConverters(Collections.<HttpMessageConverter<?>>singletonList(converter)); template.setMessageConverters(Collections.<HttpMessageConverter<?>>singletonList(converter));
ParameterizedTypeReference<List<Integer>> intList = new ParameterizedTypeReference<List<Integer>>() {}; ParameterizedTypeReference<List<Integer>> intList = new ParameterizedTypeReference<List<Integer>>() {};
expect(converter.canRead(intList.getType(), null)).andReturn(true); expect(converter.canRead(intList.getType(), null, null)).andReturn(true);
expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN)); expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(this.request); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(this.request);
HttpHeaders requestHeaders = new HttpHeaders(); HttpHeaders requestHeaders = new HttpHeaders();
@ -660,8 +660,8 @@ public class RestTemplateTests {
expect(response.getStatusCode()).andReturn(HttpStatus.OK); expect(response.getStatusCode()).andReturn(HttpStatus.OK);
expect(response.getHeaders()).andReturn(responseHeaders).times(3); expect(response.getHeaders()).andReturn(responseHeaders).times(3);
List<Integer> expected = Collections.singletonList(42); List<Integer> expected = Collections.singletonList(42);
expect(converter.canRead(intList.getType(), MediaType.TEXT_PLAIN)).andReturn(true); expect(converter.canRead(intList.getType(), null, MediaType.TEXT_PLAIN)).andReturn(true);
expect(converter.read(intList.getType(), response)).andReturn(expected); expect(converter.read(intList.getType(), null, response)).andReturn(expected);
expect(response.getStatusCode()).andReturn(HttpStatus.OK); expect(response.getStatusCode()).andReturn(HttpStatus.OK);
response.close(); response.close();

View File

@ -21,16 +21,19 @@ import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType; import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -104,40 +107,44 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
* @param <T> the expected type of the argument value to be created * @param <T> the expected type of the argument value to be created
* @param inputMessage the HTTP input message representing the current request * @param inputMessage the HTTP input message representing the current request
* @param methodParam the method argument * @param methodParam the method argument
* @param paramType the type of the argument value to be created * @param targetType the type of object to create, not necessarily the same as
* the method parameter type (e.g. for {@code HttpEntity<String>} method
* parameter the target type is String)
* @return the created method argument value * @return the created method argument value
* @throws IOException if the reading from the request fails * @throws IOException if the reading from the request fails
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage,
MethodParameter methodParam, Type paramType) throws IOException, HttpMediaTypeNotSupportedException { MethodParameter methodParam, Type targetType) throws IOException, HttpMediaTypeNotSupportedException {
MediaType contentType = inputMessage.getHeaders().getContentType(); MediaType contentType = inputMessage.getHeaders().getContentType();
if (contentType == null) { if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM; contentType = MediaType.APPLICATION_OCTET_STREAM;
} }
Class<T> paramClass = getParamClass(paramType); Class<?> contextClass = methodParam.getDeclaringClass();
Map<TypeVariable, Type> map = GenericTypeResolver.getTypeVariableMap(contextClass);
Class<T> targetClass = (Class<T>) GenericTypeResolver.resolveType(targetType, map);
for (HttpMessageConverter<?> messageConverter : this.messageConverters) { for (HttpMessageConverter<?> converter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) { if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter; GenericHttpMessageConverter genericConverter = (GenericHttpMessageConverter) converter;
if (genericMessageConverter.canRead(paramType, contentType)) { if (genericConverter.canRead(targetType, contextClass, contentType)) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Reading [" + paramType + "] as \"" + logger.debug("Reading [" + targetType + "] as \"" +
contentType + "\" using [" + messageConverter + "]"); contentType + "\" using [" + converter + "]");
} }
return (T) genericMessageConverter.read(paramType, inputMessage); return (T) genericConverter.read(targetType, contextClass, inputMessage);
} }
} }
if (paramClass != null) { if (targetClass != null) {
if (messageConverter.canRead(paramClass, contentType)) { if (converter.canRead(targetClass, contentType)) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Reading [" + paramClass.getName() + "] as \"" + contentType + "\" using [" + logger.debug("Reading [" + targetClass.getName() + "] as \"" +
messageConverter + "]"); contentType + "\" using [" + converter + "]");
} }
return ((HttpMessageConverter<T>) messageConverter).read(paramClass, inputMessage); return ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
} }
} }
} }
@ -145,26 +152,6 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes); throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes);
} }
private Class getParamClass(Type paramType) {
if (paramType instanceof Class) {
return (Class) paramType;
}
else if (paramType instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) paramType).getGenericComponentType();
if (componentType instanceof Class) {
// Surely, there should be a nicer way to determine the array type
return Array.newInstance((Class<?>) componentType, 0).getClass();
}
}
else if (paramType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) paramType;
if (parameterizedType.getRawType() instanceof Class) {
return (Class) parameterizedType.getRawType();
}
}
return null;
}
/** /**
* Creates a new {@link HttpInputMessage} from the given {@link NativeWebRequest}. * Creates a new {@link HttpInputMessage} from the given {@link NativeWebRequest}.
* *

View File

@ -18,6 +18,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import java.io.Serializable;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -26,6 +27,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
@ -35,6 +37,7 @@ import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.method.support.ModelAndViewContainer;
/** /**
@ -58,6 +61,8 @@ public class HttpEntityMethodProcessorTests {
private MockHttpServletRequest servletRequest; private MockHttpServletRequest servletRequest;
private WebDataBinderFactory binderFactory;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@ -70,6 +75,8 @@ public class HttpEntityMethodProcessorTests {
servletRequest = new MockHttpServletRequest(); servletRequest = new MockHttpServletRequest();
servletResponse = new MockHttpServletResponse(); servletResponse = new MockHttpServletResponse();
webRequest = new ServletWebRequest(servletRequest, servletResponse); webRequest = new ServletWebRequest(servletRequest, servletResponse);
binderFactory = new ValidatingBinderFactory();
} }
@Test @Test
@ -84,7 +91,7 @@ public class HttpEntityMethodProcessorTests {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
HttpEntity<SimpleBean> result = (HttpEntity<SimpleBean>) processor.resolveArgument( HttpEntity<SimpleBean> result = (HttpEntity<SimpleBean>) processor.resolveArgument(
paramSimpleBean, mavContainer, webRequest, new ValidatingBinderFactory()); paramSimpleBean, mavContainer, webRequest, binderFactory);
assertNotNull(result); assertNotNull(result);
assertEquals("Jad", result.getBody().getName()); assertEquals("Jad", result.getBody().getName());
@ -102,27 +109,68 @@ public class HttpEntityMethodProcessorTests {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
HttpEntity<List<SimpleBean>> result = (HttpEntity<List<SimpleBean>>) processor.resolveArgument( HttpEntity<List<SimpleBean>> result = (HttpEntity<List<SimpleBean>>) processor.resolveArgument(
paramList, mavContainer, webRequest, new ValidatingBinderFactory()); paramList, mavContainer, webRequest, binderFactory);
assertNotNull(result); assertNotNull(result);
assertEquals("Jad", result.getBody().get(0).getName()); assertEquals("Jad", result.getBody().get(0).getName());
assertEquals("Robert", result.getBody().get(1).getName()); assertEquals("Robert", result.getBody().get(1).getName());
} }
@Test
public void resolveArgumentTypeVariable() throws Exception {
Method method = MySimpleParameterizedController.class.getMethod("handleDto", HttpEntity.class);
HandlerMethod handlerMethod = new HandlerMethod(new MySimpleParameterizedController(), method);
MethodParameter methodParam = handlerMethod.getMethodParameters()[0];
String content = "{\"name\" : \"Jad\"}";
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters);
@SuppressWarnings("unchecked")
HttpEntity<SimpleBean> result = (HttpEntity<SimpleBean>) processor.resolveArgument(methodParam, mavContainer, webRequest, binderFactory);
assertNotNull(result);
assertEquals("Jad", result.getBody().getName());
}
public void handle(HttpEntity<List<SimpleBean>> arg1, HttpEntity<SimpleBean> arg2) { public void handle(HttpEntity<List<SimpleBean>> arg1, HttpEntity<SimpleBean> arg2) {
} }
private static abstract class MyParameterizedController<DTO extends Identifiable> {
@SuppressWarnings("unused")
public void handleDto(HttpEntity<DTO> dto) {}
}
private static class SimpleBean { private static class MySimpleParameterizedController extends MyParameterizedController<SimpleBean> { }
private interface Identifiable extends Serializable {
public Long getId();
public void setId(Long id);
}
@SuppressWarnings({ "serial" })
private static class SimpleBean implements Identifiable {
private Long id;
private String name; private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() { public String getName() {
return name; return name;
} }
@SuppressWarnings("unused")
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }

View File

@ -19,9 +19,9 @@ package org.springframework.web.servlet.mvc.method.annotation;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import java.io.Serializable;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import org.junit.Before; import org.junit.Before;
@ -42,6 +42,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.method.support.ModelAndViewContainer;
/** /**
@ -68,6 +69,9 @@ public class RequestResponseBodyMethodProcessorTests {
private MockHttpServletResponse servletResponse; private MockHttpServletResponse servletResponse;
private ValidatingBinderFactory binderFactory;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@ -85,8 +89,9 @@ public class RequestResponseBodyMethodProcessorTests {
servletRequest = new MockHttpServletRequest(); servletRequest = new MockHttpServletRequest();
servletResponse = new MockHttpServletResponse(); servletResponse = new MockHttpServletResponse();
webRequest = new ServletWebRequest(servletRequest, servletResponse); webRequest = new ServletWebRequest(servletRequest, servletResponse);
}
this.binderFactory = new ValidatingBinderFactory();
}
@Test @Test
public void resolveArgumentParameterizedType() throws Exception { public void resolveArgumentParameterizedType() throws Exception {
@ -100,7 +105,7 @@ public class RequestResponseBodyMethodProcessorTests {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<SimpleBean> result = (List<SimpleBean>) processor.resolveArgument( List<SimpleBean> result = (List<SimpleBean>) processor.resolveArgument(
paramGenericList, mavContainer, webRequest, new ValidatingBinderFactory()); paramGenericList, mavContainer, webRequest, binderFactory);
assertNotNull(result); assertNotNull(result);
assertEquals("Jad", result.get(0).getName()); assertEquals("Jad", result.get(0).getName());
@ -119,7 +124,7 @@ public class RequestResponseBodyMethodProcessorTests {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
MultiValueMap<String, String> result = (MultiValueMap<String, String>) processor.resolveArgument( MultiValueMap<String, String> result = (MultiValueMap<String, String>) processor.resolveArgument(
paramMultiValueMap, mavContainer, webRequest, new ValidatingBinderFactory()); paramMultiValueMap, mavContainer, webRequest, binderFactory);
assertNotNull(result); assertNotNull(result);
assertEquals("apple", result.getFirst("fruit")); assertEquals("apple", result.getFirst("fruit"));
@ -137,7 +142,7 @@ public class RequestResponseBodyMethodProcessorTests {
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
SimpleBean result = (SimpleBean) processor.resolveArgument( SimpleBean result = (SimpleBean) processor.resolveArgument(
paramSimpleBean, mavContainer, webRequest, new ValidatingBinderFactory()); paramSimpleBean, mavContainer, webRequest, binderFactory);
assertNotNull(result); assertNotNull(result);
assertEquals("Jad", result.getName()); assertEquals("Jad", result.getName());
@ -154,12 +159,35 @@ public class RequestResponseBodyMethodProcessorTests {
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
String result = (String) processor.resolveArgument( String result = (String) processor.resolveArgument(
paramString, mavContainer, webRequest, new ValidatingBinderFactory()); paramString, mavContainer, webRequest, binderFactory);
assertNotNull(result); assertNotNull(result);
assertEquals("foobarbaz", result); assertEquals("foobarbaz", result);
} }
// SPR-9964
@Test
public void resolveArgumentTypeVariable() throws Exception {
Method method = MySimpleParameterizedController.class.getMethod("handleDto", Identifiable.class);
HandlerMethod handlerMethod = new HandlerMethod(new MySimpleParameterizedController(), method);
MethodParameter methodParam = handlerMethod.getMethodParameters()[0];
String content = "{\"name\" : \"Jad\"}";
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
SimpleBean result = (SimpleBean) processor.resolveArgument(methodParam, mavContainer, webRequest, binderFactory);
assertNotNull(result);
assertEquals("Jad", result.getName());
}
// SPR-9160 // SPR-9160
@Test @Test
@ -213,15 +241,36 @@ public class RequestResponseBodyMethodProcessorTests {
return null; return null;
} }
private static class SimpleBean { private static abstract class MyParameterizedController<DTO extends Identifiable> {
@SuppressWarnings("unused")
public void handleDto(@RequestBody DTO dto) {}
}
private static class MySimpleParameterizedController extends MyParameterizedController<SimpleBean> { }
private interface Identifiable extends Serializable {
public Long getId();
public void setId(Long id);
}
@SuppressWarnings({ "serial" })
private static class SimpleBean implements Identifiable {
private Long id;
private String name; private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() { public String getName() {
return name; return name;
} }
@SuppressWarnings("unused")
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }