From b7f7fae78a8df52d9b6252c104fdb50ea9c0b854 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 21 Nov 2012 10:40:28 -0500 Subject: [PATCH] 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 --- .../GenericHttpMessageConverter.java | 9 ++- .../MappingJackson2HttpMessageConverter.java | 27 ++++---- .../MappingJacksonHttpMessageConverter.java | 26 ++++---- .../Jaxb2CollectionHttpMessageConverter.java | 6 +- .../client/HttpMessageConverterExtractor.java | 4 +- .../web/client/RestTemplate.java | 17 ++--- ...pingJackson2HttpMessageConverterTests.java | 6 +- ...ppingJacksonHttpMessageConverterTests.java | 6 +- ...b2CollectionHttpMessageConverterTests.java | 14 ++-- .../HttpMessageConverterExtractorTests.java | 4 +- .../web/client/RestTemplateTests.java | 10 +-- ...essageConverterMethodArgumentResolver.java | 57 +++++++--------- .../HttpEntityMethodProcessorTests.java | 56 ++++++++++++++-- ...questResponseBodyMethodProcessorTests.java | 65 ++++++++++++++++--- 14 files changed, 202 insertions(+), 105 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java index 76c56437da1..b775985c5f3 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java @@ -27,6 +27,7 @@ import org.springframework.http.MediaType; * request into a target object of a specified generic type. * * @author Arjen Poutsma + * @author Rossen Stoyanchev * @since 3.2 * * @see org.springframework.core.ParameterizedTypeReference @@ -36,11 +37,13 @@ public interface GenericHttpMessageConverter extends HttpMessageConverter /** * Indicates whether the given type can be read by this converter. * @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. * Typically the value of a {@code Content-Type} header. * @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. @@ -48,12 +51,14 @@ public interface GenericHttpMessageConverter extends HttpMessageConverter * been passed to the {@link #canRead canRead} method of this interface, * which must have returned {@code true}. * @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 * @return the converted object * @throws IOException in case of I/O 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; } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java index 2e4e6fb45bf..f2fe753839e 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java @@ -128,11 +128,11 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv @Override 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) { - JavaType javaType = getJavaType(type); + public boolean canRead(Type type, Class contextClass, MediaType mediaType) { + JavaType javaType = getJavaType(type, contextClass); return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType)); } @@ -151,14 +151,14 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - JavaType javaType = getJavaType(clazz); + JavaType javaType = getJavaType(clazz, null); return readJavaType(javaType, inputMessage); } - public Object read(Type type, HttpInputMessage inputMessage) + public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - JavaType javaType = getJavaType(type); + JavaType javaType = getJavaType(type, contextClass); return readJavaType(javaType, inputMessage); } @@ -197,10 +197,10 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv } } - /** - * Return the Jackson {@link JavaType} for the specified type. - *

The default implementation returns {@link ObjectMapper#constructType(java.lang.reflect.Type)}, + * Return the Jackson {@link JavaType} for the specified type and context class. + *

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. * For instance: *

@@ -213,10 +213,15 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
 	 * }
 	 * 
* @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 */ - protected JavaType getJavaType(Type type) { - return this.objectMapper.constructType(type); + protected JavaType getJavaType(Type type, Class contextClass) { + return (contextClass != null) ? + this.objectMapper.getTypeFactory().constructType(type, contextClass) : + this.objectMapper.constructType(type); } /** diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java index d85b6271978..81fae185c68 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java @@ -128,11 +128,11 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve @Override 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) { - JavaType javaType = getJavaType(type); + public boolean canRead(Type type, Class contextClass, MediaType mediaType) { + JavaType javaType = getJavaType(type, contextClass); return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType)); } @@ -151,14 +151,14 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - JavaType javaType = getJavaType(clazz); + JavaType javaType = getJavaType(clazz, null); return readJavaType(javaType, inputMessage); } - public Object read(Type type, HttpInputMessage inputMessage) + public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - JavaType javaType = getJavaType(type); + JavaType javaType = getJavaType(type, contextClass); return readJavaType(javaType, inputMessage); } @@ -196,10 +196,10 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve } } - /** - * Return the Jackson {@link JavaType} for the specified type. - *

The default implementation returns {@link TypeFactory#type(java.lang.reflect.Type)}, + * Return the Jackson {@link JavaType} for the specified type and context class. + *

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. * For instance: *

@@ -212,10 +212,14 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
 	 * }
 	 * 
* @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 */ - protected JavaType getJavaType(Type type) { - return TypeFactory.type(type); + protected JavaType getJavaType(Type type, Class contextClass) { + return (contextClass != null) ? + TypeFactory.type(type, TypeFactory.type(contextClass)) : + TypeFactory.type(type); } /** diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java index 376becb1dfd..af65ee275c6 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java @@ -74,7 +74,7 @@ public class Jaxb2CollectionHttpMessageConverter * {@link Collection} where the generic type is a JAXB type annotated with * {@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)) { return false; } @@ -119,7 +119,9 @@ public class Jaxb2CollectionHttpMessageConverter 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; T result = createCollection((Class) parameterizedType.getRawType()); Class elementClass = (Class) parameterizedType.getActualTypeArguments()[0]; diff --git a/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java b/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java index 7fd2dd7b108..53f83bf085c 100644 --- a/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java +++ b/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java @@ -86,12 +86,12 @@ public class HttpMessageConverterExtractor implements ResponseExtractor { for (HttpMessageConverter messageConverter : this.messageConverters) { if (messageConverter instanceof GenericHttpMessageConverter) { GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter; - if (genericMessageConverter.canRead(this.responseType, contentType)) { + if (genericMessageConverter.canRead(this.responseType, null, contentType)) { if (logger.isDebugEnabled()) { logger.debug("Reading [" + this.responseType + "] as \"" + contentType + "\" using [" + messageConverter + "]"); } - return (T) genericMessageConverter.read(this.responseType, response); + return (T) genericMessageConverter.read(this.responseType, null, response); } } if (this.responseClass != null) { diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index ba29e8ac670..b9006b849c0 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -553,20 +553,17 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat } List allSupportedMediaTypes = new ArrayList(); - for (HttpMessageConverter messageConverter : getMessageConverters()) { + for (HttpMessageConverter converter : getMessageConverters()) { if (responseClass != null) { - if (messageConverter.canRead(responseClass, null)) { - allSupportedMediaTypes - .addAll(getSupportedMediaTypes(messageConverter)); + if (converter.canRead(responseClass, null)) { + allSupportedMediaTypes.addAll(getSupportedMediaTypes(converter)); } } - else if (messageConverter instanceof GenericHttpMessageConverter) { + else if (converter instanceof GenericHttpMessageConverter) { - GenericHttpMessageConverter genericMessageConverter = - (GenericHttpMessageConverter) messageConverter; - if (genericMessageConverter.canRead(responseType, null)) { - allSupportedMediaTypes - .addAll(getSupportedMediaTypes(messageConverter)); + GenericHttpMessageConverter genericConverter = (GenericHttpMessageConverter) converter; + if (genericConverter.canRead(responseType, null, null)) { + allSupportedMediaTypes.addAll(getSupportedMediaTypes(converter)); } } diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java index 3579c5d4644..a755a7458f5 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java @@ -51,12 +51,12 @@ public class MappingJackson2HttpMessageConverterTests extends AbstractMappingJac MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter() { @Override - protected JavaType getJavaType(Type type) { + protected JavaType getJavaType(Type type, Class contextClass) { if (type instanceof Class && List.class.isAssignableFrom((Class)type)) { return new ObjectMapper().getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class); } 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")); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); - List results = (List) converter.read(beansList.getType(), inputMessage); + List results = (List) converter.read(beansList.getType(), null, inputMessage); assertEquals(1, results.size()); MyBean result = results.get(0); assertEquals("Foo", result.getString()); diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java index 5f70a4d6d4b..424bb885c8c 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java @@ -49,12 +49,12 @@ public class MappingJacksonHttpMessageConverterTests extends AbstractMappingJack public void readGenerics() throws IOException { MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter() { @Override - protected JavaType getJavaType(Type type) { + protected JavaType getJavaType(Type type, Class contextClass) { if (type instanceof Class && List.class.isAssignableFrom((Class)type)) { return TypeFactory.collectionType(ArrayList.class, MyBean.class); } 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")); MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter(); - List results = (List) converter.read(beansList.getType(), inputMessage); + List results = (List) converter.read(beansList.getType(), null, inputMessage); assertEquals(1, results.size()); MyBean result = results.get(0); assertEquals("Foo", result.getString()); diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java index 44715ac6cbe..ad189fd6625 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java @@ -63,9 +63,9 @@ public class Jaxb2CollectionHttpMessageConverterTests { @Test public void canRead() throws Exception { - assertTrue(converter.canRead(rootElementListType, null)); - assertTrue(converter.canRead(rootElementSetType, null)); - assertTrue(converter.canRead(typeSetType, null)); + assertTrue(converter.canRead(rootElementListType, null, null)); + assertTrue(converter.canRead(rootElementSetType, null, null)); + assertTrue(converter.canRead(typeSetType, null, null)); } @Test @@ -74,7 +74,7 @@ public class Jaxb2CollectionHttpMessageConverterTests { String content = ""; MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); - List result = (List) converter.read(rootElementListType, inputMessage); + List result = (List) converter.read(rootElementListType, null, inputMessage); assertEquals("Invalid result", 2, result.size()); assertEquals("Invalid result", "1", result.get(0).type.s); @@ -87,7 +87,7 @@ public class Jaxb2CollectionHttpMessageConverterTests { String content = ""; MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); - Set result = (Set) converter.read(rootElementSetType, inputMessage); + Set result = (Set) converter.read(rootElementSetType, null, inputMessage); assertEquals("Invalid result", 2, result.size()); assertTrue("Invalid result", result.contains(new RootElement("1"))); @@ -100,7 +100,7 @@ public class Jaxb2CollectionHttpMessageConverterTests { String content = ""; MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); - List result = (List) converter.read(typeListType, inputMessage); + List result = (List) converter.read(typeListType, null, inputMessage); assertEquals("Invalid result", 2, result.size()); assertEquals("Invalid result", "1", result.get(0).s); @@ -113,7 +113,7 @@ public class Jaxb2CollectionHttpMessageConverterTests { String content = ""; MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); - Set result = (Set) converter.read(typeSetType, inputMessage); + Set result = (Set) converter.read(typeSetType, null, inputMessage); assertEquals("Invalid result", 2, result.size()); assertTrue("Invalid result", result.contains(new TestType("1"))); diff --git a/spring-web/src/test/java/org/springframework/web/client/HttpMessageConverterExtractorTests.java b/spring-web/src/test/java/org/springframework/web/client/HttpMessageConverterExtractorTests.java index a8e9a418e66..f9c587248ec 100644 --- a/spring-web/src/test/java/org/springframework/web/client/HttpMessageConverterExtractorTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/HttpMessageConverterExtractorTests.java @@ -176,8 +176,8 @@ public class HttpMessageConverterExtractorTests { expect(response.getStatusCode()).andReturn(HttpStatus.OK); expect(response.getHeaders()).andReturn(responseHeaders).times(2); - expect(converter.canRead(type, contentType)).andReturn(true); - expect(converter.read(type, response)).andReturn(expected); + expect(converter.canRead(type, null, contentType)).andReturn(true); + expect(converter.read(type, null, response)).andReturn(expected); replay(response, converter); Object result = extractor.extractData(response); diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index d12c5cf8e39..26f37bd638f 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -86,7 +86,7 @@ public class RestTemplateTests { verifyMocks(); } - + @Test public void varArgsNullTemplateVariable() throws Exception { expect(requestFactory.createRequest(new URI("http://example.com/-foo"), HttpMethod.GET)) @@ -481,7 +481,7 @@ public class RestTemplateTests { verifyMocks(); } - + @Test public void postForEntityNull() throws Exception { MediaType textPlain = new MediaType("text", "plain"); @@ -644,7 +644,7 @@ public class RestTemplateTests { template.setMessageConverters(Collections.>singletonList(converter)); ParameterizedTypeReference> intList = new ParameterizedTypeReference>() {}; - 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(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(this.request); HttpHeaders requestHeaders = new HttpHeaders(); @@ -660,8 +660,8 @@ public class RestTemplateTests { expect(response.getStatusCode()).andReturn(HttpStatus.OK); expect(response.getHeaders()).andReturn(responseHeaders).times(3); List expected = Collections.singletonList(42); - expect(converter.canRead(intList.getType(), MediaType.TEXT_PLAIN)).andReturn(true); - expect(converter.read(intList.getType(), response)).andReturn(expected); + expect(converter.canRead(intList.getType(), null, MediaType.TEXT_PLAIN)).andReturn(true); + expect(converter.read(intList.getType(), null, response)).andReturn(expected); expect(response.getStatusCode()).andReturn(HttpStatus.OK); response.close(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index a27b4ac9981..b462214e024 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -21,16 +21,19 @@ import java.lang.reflect.Array; import java.lang.reflect.GenericArrayType; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.MediaType; @@ -104,40 +107,44 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements * @param the expected type of the argument value to be created * @param inputMessage the HTTP input message representing the current request * @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} method + * parameter the target type is String) * @return the created method argument value * @throws IOException if the reading from the request fails * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found */ @SuppressWarnings("unchecked") protected Object readWithMessageConverters(HttpInputMessage inputMessage, - MethodParameter methodParam, Type paramType) throws IOException, HttpMediaTypeNotSupportedException { + MethodParameter methodParam, Type targetType) throws IOException, HttpMediaTypeNotSupportedException { MediaType contentType = inputMessage.getHeaders().getContentType(); if (contentType == null) { contentType = MediaType.APPLICATION_OCTET_STREAM; } - Class paramClass = getParamClass(paramType); + Class contextClass = methodParam.getDeclaringClass(); + Map map = GenericTypeResolver.getTypeVariableMap(contextClass); + Class targetClass = (Class) GenericTypeResolver.resolveType(targetType, map); - for (HttpMessageConverter messageConverter : this.messageConverters) { - if (messageConverter instanceof GenericHttpMessageConverter) { - GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter; - if (genericMessageConverter.canRead(paramType, contentType)) { + for (HttpMessageConverter converter : this.messageConverters) { + if (converter instanceof GenericHttpMessageConverter) { + GenericHttpMessageConverter genericConverter = (GenericHttpMessageConverter) converter; + if (genericConverter.canRead(targetType, contextClass, contentType)) { if (logger.isDebugEnabled()) { - logger.debug("Reading [" + paramType + "] as \"" + - contentType + "\" using [" + messageConverter + "]"); + logger.debug("Reading [" + targetType + "] as \"" + + contentType + "\" using [" + converter + "]"); } - return (T) genericMessageConverter.read(paramType, inputMessage); + return (T) genericConverter.read(targetType, contextClass, inputMessage); } } - if (paramClass != null) { - if (messageConverter.canRead(paramClass, contentType)) { + if (targetClass != null) { + if (converter.canRead(targetClass, contentType)) { if (logger.isDebugEnabled()) { - logger.debug("Reading [" + paramClass.getName() + "] as \"" + contentType + "\" using [" + - messageConverter + "]"); + logger.debug("Reading [" + targetClass.getName() + "] as \"" + + contentType + "\" using [" + converter + "]"); } - return ((HttpMessageConverter) messageConverter).read(paramClass, inputMessage); + return ((HttpMessageConverter) converter).read(targetClass, inputMessage); } } } @@ -145,26 +152,6 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements 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}. * diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorTests.java index 4660c44f6c5..5c5aed87247 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorTests.java @@ -18,6 +18,7 @@ package org.springframework.web.servlet.mvc.method.annotation; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @@ -26,6 +27,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.core.MethodParameter; import org.springframework.http.HttpEntity; +import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 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.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.ModelAndViewContainer; /** @@ -58,6 +61,8 @@ public class HttpEntityMethodProcessorTests { private MockHttpServletRequest servletRequest; + private WebDataBinderFactory binderFactory; + @Before public void setUp() throws Exception { @@ -70,6 +75,8 @@ public class HttpEntityMethodProcessorTests { servletRequest = new MockHttpServletRequest(); servletResponse = new MockHttpServletResponse(); webRequest = new ServletWebRequest(servletRequest, servletResponse); + + binderFactory = new ValidatingBinderFactory(); } @Test @@ -84,7 +91,7 @@ public class HttpEntityMethodProcessorTests { @SuppressWarnings("unchecked") HttpEntity result = (HttpEntity) processor.resolveArgument( - paramSimpleBean, mavContainer, webRequest, new ValidatingBinderFactory()); + paramSimpleBean, mavContainer, webRequest, binderFactory); assertNotNull(result); assertEquals("Jad", result.getBody().getName()); @@ -102,27 +109,68 @@ public class HttpEntityMethodProcessorTests { @SuppressWarnings("unchecked") HttpEntity> result = (HttpEntity>) processor.resolveArgument( - paramList, mavContainer, webRequest, new ValidatingBinderFactory()); + paramList, mavContainer, webRequest, binderFactory); assertNotNull(result); assertEquals("Jad", result.getBody().get(0).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> converters = new ArrayList>(); + converters.add(new MappingJackson2HttpMessageConverter()); + HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters); + + @SuppressWarnings("unchecked") + HttpEntity result = (HttpEntity) processor.resolveArgument(methodParam, mavContainer, webRequest, binderFactory); + + assertNotNull(result); + assertEquals("Jad", result.getBody().getName()); + } public void handle(HttpEntity> arg1, HttpEntity arg2) { } + private static abstract class MyParameterizedController { + @SuppressWarnings("unused") + public void handleDto(HttpEntity dto) {} + } - private static class SimpleBean { + private static class MySimpleParameterizedController extends MyParameterizedController { } + 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; + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + public String getName() { return name; } - @SuppressWarnings("unused") public void setName(String name) { this.name = name; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java index 907039a008b..6c2dcdb56a3 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java @@ -19,9 +19,9 @@ package org.springframework.web.servlet.mvc.method.annotation; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Collection; import java.util.List; 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.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.ModelAndViewContainer; /** @@ -68,6 +69,9 @@ public class RequestResponseBodyMethodProcessorTests { private MockHttpServletResponse servletResponse; + private ValidatingBinderFactory binderFactory; + + @Before public void setUp() throws Exception { @@ -85,8 +89,9 @@ public class RequestResponseBodyMethodProcessorTests { servletRequest = new MockHttpServletRequest(); servletResponse = new MockHttpServletResponse(); webRequest = new ServletWebRequest(servletRequest, servletResponse); - } + this.binderFactory = new ValidatingBinderFactory(); + } @Test public void resolveArgumentParameterizedType() throws Exception { @@ -100,7 +105,7 @@ public class RequestResponseBodyMethodProcessorTests { @SuppressWarnings("unchecked") List result = (List) processor.resolveArgument( - paramGenericList, mavContainer, webRequest, new ValidatingBinderFactory()); + paramGenericList, mavContainer, webRequest, binderFactory); assertNotNull(result); assertEquals("Jad", result.get(0).getName()); @@ -119,7 +124,7 @@ public class RequestResponseBodyMethodProcessorTests { @SuppressWarnings("unchecked") MultiValueMap result = (MultiValueMap) processor.resolveArgument( - paramMultiValueMap, mavContainer, webRequest, new ValidatingBinderFactory()); + paramMultiValueMap, mavContainer, webRequest, binderFactory); assertNotNull(result); assertEquals("apple", result.getFirst("fruit")); @@ -137,7 +142,7 @@ public class RequestResponseBodyMethodProcessorTests { RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); SimpleBean result = (SimpleBean) processor.resolveArgument( - paramSimpleBean, mavContainer, webRequest, new ValidatingBinderFactory()); + paramSimpleBean, mavContainer, webRequest, binderFactory); assertNotNull(result); assertEquals("Jad", result.getName()); @@ -154,12 +159,35 @@ public class RequestResponseBodyMethodProcessorTests { RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); String result = (String) processor.resolveArgument( - paramString, mavContainer, webRequest, new ValidatingBinderFactory()); + paramString, mavContainer, webRequest, binderFactory); assertNotNull(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> converters = new ArrayList>(); + 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 @Test @@ -213,15 +241,36 @@ public class RequestResponseBodyMethodProcessorTests { return null; } - private static class SimpleBean { + private static abstract class MyParameterizedController { + @SuppressWarnings("unused") + public void handleDto(@RequestBody DTO dto) {} + } + private static class MySimpleParameterizedController extends MyParameterizedController { } + + 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; + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + public String getName() { return name; } - @SuppressWarnings("unused") public void setName(String name) { this.name = name; }