Support generic types in @RequestBody arguments

This change makes it possible to declare an @RequestBody argument with
a generic type (e.g. List<Foo>). If a GenericHttpMessageConverter
implementation supports the method argument, then the request will be
converted to the apropiate target type.

The new GenericHttpMessageConverter is implemented by the
MappingJacksonHttpMessageConverter and also by a new
Jaxb2CollectionHttpMessageConverter that can read read a generic
Collection where the generic type is a JAXB type annotated with
@XmlRootElement or @XmlType.

Issue: SPR-9570
This commit is contained in:
Rossen Stoyanchev 2012-08-23 09:46:54 -04:00
parent ed3823b045
commit c9b7b132fb
8 changed files with 819 additions and 477 deletions

View File

@ -42,6 +42,8 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
private final Type responseType;
private final Class<T> responseClass;
private final List<HttpMessageConverter<?>> messageConverters;
private final Log logger;
@ -64,10 +66,12 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
this(responseType, messageConverters, LogFactory.getLog(HttpMessageConverterExtractor.class));
}
@SuppressWarnings("unchecked")
HttpMessageConverterExtractor(Type responseType, List<HttpMessageConverter<?>> messageConverters, Log logger) {
Assert.notNull(responseType, "'responseType' must not be null");
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.responseType = responseType;
this.responseClass = (responseType instanceof Class) ? (Class<T>) responseType : null;
this.messageConverters = messageConverters;
this.logger = logger;
}
@ -79,21 +83,8 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
}
MediaType contentType = getContentType(response);
Class<T> responseClass = null;
if (this.responseType instanceof Class) {
responseClass = (Class) this.responseType;
}
for (HttpMessageConverter messageConverter : this.messageConverters) {
if (responseClass != null) {
if (messageConverter.canRead(responseClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + responseClass.getName() + "] as \"" +
contentType + "\" using [" + messageConverter + "]");
}
return (T) messageConverter.read(responseClass, response);
}
}
else if (messageConverter instanceof GenericHttpMessageConverter) {
if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter;
if (genericMessageConverter.canRead(this.responseType, contentType)) {
if (logger.isDebugEnabled()) {
@ -103,6 +94,15 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
return (T) genericMessageConverter.read(this.responseType, response);
}
}
if (this.responseClass != null) {
if (messageConverter.canRead(this.responseClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + this.responseClass.getName() + "] as \"" +
contentType + "\" using [" + messageConverter + "]");
}
return (T) messageConverter.read(this.responseClass, response);
}
}
}
throw new RestClientException(
"Could not extract response: no suitable HttpMessageConverter found for response type [" +

View File

@ -17,6 +17,7 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
@ -30,6 +31,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.Assert;
@ -85,8 +87,8 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
* @throws IOException if the reading from the request fails
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
*/
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam, Class<T> paramType) throws IOException,
HttpMediaTypeNotSupportedException {
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest,
MethodParameter methodParam, Type paramType) throws IOException, HttpMediaTypeNotSupportedException {
HttpInputMessage inputMessage = createInputMessage(webRequest);
return readWithMessageConverters(inputMessage, methodParam, paramType);
@ -106,20 +108,34 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
*/
@SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter methodParam,
Class<T> paramType) throws IOException, HttpMediaTypeNotSupportedException {
Type paramType) throws IOException, HttpMediaTypeNotSupportedException {
MediaType contentType = inputMessage.getHeaders().getContentType();
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
Class<T> paramClass = (paramType instanceof Class) ? (Class) paramType : null;
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter.canRead(paramType, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + paramType.getName() + "] as \"" + contentType + "\" using [" +
messageConverter + "]");
if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter;
if (genericMessageConverter.canRead(paramType, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + paramType + "] as \"" +
contentType + "\" using [" + messageConverter + "]");
}
return (T) genericMessageConverter.read(paramType, inputMessage);
}
}
if (paramClass != null) {
if (messageConverter.canRead(paramClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + paramClass.getName() + "] as \"" + contentType + "\" using [" +
messageConverter + "]");
}
return ((HttpMessageConverter<T>) messageConverter).read(paramClass, inputMessage);
}
return ((HttpMessageConverter<T>) messageConverter).read(paramType, inputMessage);
}
}

View File

@ -79,13 +79,13 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
throws IOException, HttpMediaTypeNotSupportedException {
HttpInputMessage inputMessage = createInputMessage(webRequest);
Class<?> paramType = getHttpEntityType(parameter);
Type paramType = getHttpEntityType(parameter);
Object body = readWithMessageConverters(webRequest, parameter, paramType);
return new HttpEntity<Object>(body, inputMessage.getHeaders());
}
private Class<?> getHttpEntityType(MethodParameter parameter) {
private Type getHttpEntityType(MethodParameter parameter) {
Assert.isAssignable(HttpEntity.class, parameter.getParameterType());
ParameterizedType type = (ParameterizedType) parameter.getGenericParameterType();
if (type.getActualTypeArguments().length == 1) {
@ -97,10 +97,12 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
Type componentType = ((GenericArrayType) typeArgument).getGenericComponentType();
if (componentType instanceof Class) {
// Surely, there should be a nicer way to determine the array type
Object array = Array.newInstance((Class<?>) componentType, 0);
return array.getClass();
return Array.newInstance((Class<?>) componentType, 0).getClass();
}
}
else if (typeArgument instanceof ParameterizedType) {
return typeArgument;
}
}
throw new IllegalArgumentException("HttpEntity parameter (" + parameter.getParameterName() + ") "
+ "in method " + parameter.getMethod() + "is not parameterized");

View File

@ -18,6 +18,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.List;
import org.springframework.core.Conventions;
@ -86,7 +87,7 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Object argument = readWithMessageConverters(webRequest, parameter, parameter.getParameterType());
Object argument = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, argument, name);
@ -134,7 +135,7 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
@Override
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage,
MethodParameter methodParam, Class<T> paramType) throws IOException, HttpMediaTypeNotSupportedException {
MethodParameter methodParam, Type paramType) throws IOException, HttpMediaTypeNotSupportedException {
if (inputMessage.getBody() != null) {
return super.readWithMessageConverters(inputMessage, methodParam, paramType);

View File

@ -0,0 +1,305 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import org.easymock.Capture;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor;
/**
* Test fixture for {@link HttpEntityMethodProcessor} delegating to a mock
* {@link HttpMessageConverter}.
*
* <p>Also see {@link HttpEntityMethodProcessorTests}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
*/
public class HttpEntityMethodProcessorMockTests {
private HttpEntityMethodProcessor processor;
private HttpMessageConverter<String> messageConverter;
private MethodParameter paramHttpEntity;
private MethodParameter paramResponseEntity;
private MethodParameter paramInt;
private MethodParameter returnTypeResponseEntity;
private MethodParameter returnTypeHttpEntity;
private MethodParameter returnTypeInt;
private MethodParameter returnTypeResponseEntityProduces;
private ModelAndViewContainer mavContainer;
private ServletWebRequest webRequest;
private MockHttpServletResponse servletResponse;
private MockHttpServletRequest servletRequest;
@SuppressWarnings("unchecked")
@Before
public void setUp() throws Exception {
messageConverter = createMock(HttpMessageConverter.class);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
replay(messageConverter);
processor = new HttpEntityMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(messageConverter));
reset(messageConverter);
Method handle1 = getClass().getMethod("handle1", HttpEntity.class, ResponseEntity.class, Integer.TYPE);
paramHttpEntity = new MethodParameter(handle1, 0);
paramResponseEntity = new MethodParameter(handle1, 1);
paramInt = new MethodParameter(handle1, 2);
returnTypeResponseEntity = new MethodParameter(handle1, -1);
returnTypeHttpEntity = new MethodParameter(getClass().getMethod("handle2", HttpEntity.class), -1);
returnTypeInt = new MethodParameter(getClass().getMethod("handle3"), -1);
returnTypeResponseEntityProduces = new MethodParameter(getClass().getMethod("handle4"), -1);
mavContainer = new ModelAndViewContainer();
servletRequest = new MockHttpServletRequest();
servletResponse = new MockHttpServletResponse();
webRequest = new ServletWebRequest(servletRequest, servletResponse);
}
@Test
public void supportsParameter() {
assertTrue("HttpEntity parameter not supported", processor.supportsParameter(paramHttpEntity));
assertFalse("ResponseEntity parameter supported", processor.supportsParameter(paramResponseEntity));
assertFalse("non-entity parameter supported", processor.supportsParameter(paramInt));
}
@Test
public void supportsReturnType() {
assertTrue("ResponseEntity return type not supported", processor.supportsReturnType(returnTypeResponseEntity));
assertTrue("HttpEntity return type not supported", processor.supportsReturnType(returnTypeHttpEntity));
assertFalse("non-ResponseBody return type supported", processor.supportsReturnType(returnTypeInt));
}
@Test
public void resolveArgument() throws Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
String body = "Foo";
expect(messageConverter.canRead(String.class, contentType)).andReturn(true);
expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(body);
replay(messageConverter);
Object result = processor.resolveArgument(paramHttpEntity, mavContainer, webRequest, null);
assertTrue(result instanceof HttpEntity);
assertFalse("The requestHandled flag shouldn't change", mavContainer.isRequestHandled());
assertEquals("Invalid argument", body, ((HttpEntity<?>) result).getBody());
verify(messageConverter);
}
@Test(expected = HttpMediaTypeNotSupportedException.class)
public void resolveArgumentNotReadable() throws Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(contentType));
expect(messageConverter.canRead(String.class, contentType)).andReturn(false);
replay(messageConverter);
processor.resolveArgument(paramHttpEntity, mavContainer, webRequest, null);
fail("Expected exception");
}
@Test(expected = HttpMediaTypeNotSupportedException.class)
public void resolveArgumentNoContentType() throws Exception {
processor.resolveArgument(paramHttpEntity, mavContainer, webRequest, null);
fail("Expected exception");
}
@Test
public void handleReturnValue() throws Exception {
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<String>(body, HttpStatus.OK);
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(true);
messageConverter.write(eq(body), eq(accepted), isA(HttpOutputMessage.class));
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
verify(messageConverter);
}
@Test
public void handleReturnValueProduces() throws Exception {
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<String>(body, HttpStatus.OK);
servletRequest.addHeader("Accept", "text/*");
servletRequest.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Collections.singleton(MediaType.TEXT_HTML));
expect(messageConverter.canWrite(String.class, MediaType.TEXT_HTML)).andReturn(true);
messageConverter.write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntityProduces, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
verify(messageConverter);
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptable() throws Exception {
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<String>(body, HttpStatus.OK);
MediaType accepted = MediaType.APPLICATION_ATOM_XML;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
fail("Expected exception");
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptableProduces() throws Exception {
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<String>(body, HttpStatus.OK);
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntityProduces, mavContainer, webRequest);
fail("Expected exception");
}
// SPR-9142
@Test(expected=HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptableParseError() throws Exception {
ResponseEntity<String> returnValue = new ResponseEntity<String>("Body", HttpStatus.ACCEPTED);
servletRequest.addHeader("Accept", "01");
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
fail("Expected exception");
}
@Test
public void responseHeaderNoBody() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.set("headerName", "headerValue");
ResponseEntity<String> returnValue = new ResponseEntity<String>(headers, HttpStatus.ACCEPTED);
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
assertEquals("headerValue", servletResponse.getHeader("headerName"));
}
@Test
public void responseHeaderAndBody() throws Exception {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("header", "headerValue");
ResponseEntity<String> returnValue = new ResponseEntity<String>("body", responseHeaders, HttpStatus.ACCEPTED);
Capture<HttpOutputMessage> outputMessage = new Capture<HttpOutputMessage>();
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, MediaType.TEXT_PLAIN)).andReturn(true);
messageConverter.write(eq("body"), eq(MediaType.TEXT_PLAIN), capture(outputMessage));
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
assertEquals("headerValue", outputMessage.getValue().getHeaders().get("header").get(0));
verify(messageConverter);
}
public ResponseEntity<String> handle1(HttpEntity<String> httpEntity, ResponseEntity<String> responseEntity, int i) {
return responseEntity;
}
public HttpEntity<?> handle2(HttpEntity<?> entity) {
return entity;
}
public int handle3() {
return 42;
}
@RequestMapping(produces = {"text/html", "application/xhtml+xml"})
public ResponseEntity<String> handle4() {
return null;
}
}

View File

@ -15,64 +15,40 @@
*/
package org.springframework.web.servlet.mvc.method.annotation;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
import static org.junit.Assert.assertNotNull;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import org.easymock.Capture;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
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.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor;
/**
* Test fixture with {@link HttpEntityMethodProcessor} and mock {@link HttpMessageConverter}.
* Test fixture with {@link HttpEntityMethodProcessor} delegating to
* actual {@link HttpMessageConverter} instances.
*
* <p>Also see {@link HttpEntityMethodProcessorMockTests}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
*/
public class HttpEntityMethodProcessorTests {
private HttpEntityMethodProcessor processor;
private HttpMessageConverter<String> messageConverter;
private MethodParameter paramHttpEntity;
private MethodParameter paramResponseEntity;
private MethodParameter paramInt;
private MethodParameter returnTypeResponseEntity;
private MethodParameter returnTypeHttpEntity;
private MethodParameter returnTypeInt;
private MethodParameter returnTypeResponseEntityProduces;
private MethodParameter paramList;
private MethodParameter paramSimpleBean;
private ModelAndViewContainer mavContainer;
@ -82,28 +58,12 @@ public class HttpEntityMethodProcessorTests {
private MockHttpServletRequest servletRequest;
@SuppressWarnings("unchecked")
@Before
public void setUp() throws Exception {
messageConverter = createMock(HttpMessageConverter.class);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
replay(messageConverter);
processor = new HttpEntityMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(messageConverter));
reset(messageConverter);
Method handle1 = getClass().getMethod("handle1", HttpEntity.class, ResponseEntity.class, Integer.TYPE);
paramHttpEntity = new MethodParameter(handle1, 0);
paramResponseEntity = new MethodParameter(handle1, 1);
paramInt = new MethodParameter(handle1, 2);
returnTypeResponseEntity = new MethodParameter(handle1, -1);
returnTypeHttpEntity = new MethodParameter(getClass().getMethod("handle2", HttpEntity.class), -1);
returnTypeInt = new MethodParameter(getClass().getMethod("handle3"), -1);
returnTypeResponseEntityProduces = new MethodParameter(getClass().getMethod("handle4"), -1);
Method method = getClass().getMethod("handle", HttpEntity.class, HttpEntity.class);
paramList = new MethodParameter(method, 0);
paramSimpleBean = new MethodParameter(method, 1);
mavContainer = new ModelAndViewContainer();
@ -112,191 +72,70 @@ public class HttpEntityMethodProcessorTests {
webRequest = new ServletWebRequest(servletRequest, servletResponse);
}
@Test
public void supportsParameter() {
assertTrue("HttpEntity parameter not supported", processor.supportsParameter(paramHttpEntity));
assertFalse("ResponseEntity parameter supported", processor.supportsParameter(paramResponseEntity));
assertFalse("non-entity parameter supported", processor.supportsParameter(paramInt));
}
@Test
public void supportsReturnType() {
assertTrue("ResponseEntity return type not supported", processor.supportsReturnType(returnTypeResponseEntity));
assertTrue("HttpEntity return type not supported", processor.supportsReturnType(returnTypeHttpEntity));
assertFalse("non-ResponseBody return type supported", processor.supportsReturnType(returnTypeInt));
}
@Test
public void resolveArgument() throws Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
String content = "{\"name\" : \"Jad\"}";
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType("application/json");
String body = "Foo";
expect(messageConverter.canRead(String.class, contentType)).andReturn(true);
expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(body);
replay(messageConverter);
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters);
Object result = processor.resolveArgument(paramHttpEntity, mavContainer, webRequest, null);
@SuppressWarnings("unchecked")
HttpEntity<SimpleBean> result = (HttpEntity<SimpleBean>) processor.resolveArgument(
paramSimpleBean, mavContainer, webRequest, new ValidatingBinderFactory());
assertTrue(result instanceof HttpEntity);
assertFalse("The requestHandled flag shouldn't change", mavContainer.isRequestHandled());
assertEquals("Invalid argument", body, ((HttpEntity<?>) result).getBody());
verify(messageConverter);
}
@Test(expected = HttpMediaTypeNotSupportedException.class)
public void resolveArgumentNotReadable() throws Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(contentType));
expect(messageConverter.canRead(String.class, contentType)).andReturn(false);
replay(messageConverter);
processor.resolveArgument(paramHttpEntity, mavContainer, webRequest, null);
fail("Expected exception");
}
@Test(expected = HttpMediaTypeNotSupportedException.class)
public void resolveArgumentNoContentType() throws Exception {
processor.resolveArgument(paramHttpEntity, mavContainer, webRequest, null);
fail("Expected exception");
assertNotNull(result);
assertEquals("Jad", result.getBody().getName());
}
@Test
public void handleReturnValue() throws Exception {
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<String>(body, HttpStatus.OK);
public void resolveGenericArgument() throws Exception {
String content = "[{\"name\" : \"Jad\"}, {\"name\" : \"Robert\"}]";
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType("application/json");
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters);
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(true);
messageConverter.write(eq(body), eq(accepted), isA(HttpOutputMessage.class));
replay(messageConverter);
@SuppressWarnings("unchecked")
HttpEntity<List<SimpleBean>> result = (HttpEntity<List<SimpleBean>>) processor.resolveArgument(
paramList, mavContainer, webRequest, new ValidatingBinderFactory());
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
verify(messageConverter);
assertNotNull(result);
assertEquals("Jad", result.getBody().get(0).getName());
assertEquals("Robert", result.getBody().get(1).getName());
}
@Test
public void handleReturnValueProduces() throws Exception {
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<String>(body, HttpStatus.OK);
servletRequest.addHeader("Accept", "text/*");
servletRequest.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Collections.singleton(MediaType.TEXT_HTML));
expect(messageConverter.canWrite(String.class, MediaType.TEXT_HTML)).andReturn(true);
messageConverter.write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntityProduces, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
verify(messageConverter);
public void handle(HttpEntity<List<SimpleBean>> arg1, HttpEntity<SimpleBean> arg2) {
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptable() throws Exception {
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<String>(body, HttpStatus.OK);
MediaType accepted = MediaType.APPLICATION_ATOM_XML;
servletRequest.addHeader("Accept", accepted.toString());
private static class SimpleBean {
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
replay(messageConverter);
private String name;
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
public String getName() {
return name;
}
fail("Expected exception");
@SuppressWarnings("unused")
public void setName(String name) {
this.name = name;
}
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptableProduces() throws Exception {
String body = "Foo";
ResponseEntity<String> returnValue = new ResponseEntity<String>(body, HttpStatus.OK);
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntityProduces, mavContainer, webRequest);
fail("Expected exception");
private final class ValidatingBinderFactory implements WebDataBinderFactory {
public WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
WebDataBinder dataBinder = new WebDataBinder(target, objectName);
dataBinder.setValidator(validator);
return dataBinder;
}
}
// SPR-9142
@Test(expected=HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptableParseError() throws Exception {
ResponseEntity<String> returnValue = new ResponseEntity<String>("Body", HttpStatus.ACCEPTED);
servletRequest.addHeader("Accept", "01");
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
fail("Expected exception");
}
@Test
public void responseHeaderNoBody() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.set("headerName", "headerValue");
ResponseEntity<String> returnValue = new ResponseEntity<String>(headers, HttpStatus.ACCEPTED);
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
assertEquals("headerValue", servletResponse.getHeader("headerName"));
}
@Test
public void responseHeaderAndBody() throws Exception {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("header", "headerValue");
ResponseEntity<String> returnValue = new ResponseEntity<String>("body", responseHeaders, HttpStatus.ACCEPTED);
Capture<HttpOutputMessage> outputMessage = new Capture<HttpOutputMessage>();
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, MediaType.TEXT_PLAIN)).andReturn(true);
messageConverter.write(eq("body"), eq(MediaType.TEXT_PLAIN), capture(outputMessage));
replay(messageConverter);
processor.handleReturnValue(returnValue, returnTypeResponseEntity, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
assertEquals("headerValue", outputMessage.getValue().getHeaders().get("header").get(0));
verify(messageConverter);
}
public ResponseEntity<String> handle1(HttpEntity<String> httpEntity, ResponseEntity<String> responseEntity, int i) {
return responseEntity;
}
public HttpEntity<?> handle2(HttpEntity<?> entity) {
return entity;
}
public int handle3() {
return 42;
}
@RequestMapping(produces = {"text/html", "application/xhtml+xml"})
public ResponseEntity<String> handle4() {
return null;
}
}

View File

@ -0,0 +1,356 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
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.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
/**
* Test fixture for {@link RequestResponseBodyMethodProcessor} delegating to a
* mock HttpMessageConverter.
*
* <p>Also see {@link RequestResponseBodyMethodProcessorTests}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
*/
public class RequestResponseBodyMethodProcessorMockTests {
private RequestResponseBodyMethodProcessor processor;
private HttpMessageConverter<String> messageConverter;
private MethodParameter paramRequestBodyString;
private MethodParameter paramInt;
private MethodParameter paramValidBean;
private MethodParameter paramStringNotRequired;
private MethodParameter returnTypeString;
private MethodParameter returnTypeInt;
private MethodParameter returnTypeStringProduces;
private ModelAndViewContainer mavContainer;
private NativeWebRequest webRequest;
private MockHttpServletRequest servletRequest;
private MockHttpServletResponse servletResponse;
@SuppressWarnings("unchecked")
@Before
public void setUp() throws Exception {
messageConverter = createMock(HttpMessageConverter.class);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
replay(messageConverter);
processor = new RequestResponseBodyMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(messageConverter));
reset(messageConverter);
Method methodHandle1 = getClass().getMethod("handle1", String.class, Integer.TYPE);
paramRequestBodyString = new MethodParameter(methodHandle1, 0);
paramInt = new MethodParameter(methodHandle1, 1);
returnTypeString = new MethodParameter(methodHandle1, -1);
returnTypeInt = new MethodParameter(getClass().getMethod("handle2"), -1);
returnTypeStringProduces = new MethodParameter(getClass().getMethod("handle3"), -1);
paramValidBean = new MethodParameter(getClass().getMethod("handle4", SimpleBean.class), 0);
paramStringNotRequired = new MethodParameter(getClass().getMethod("handle5", String.class), 0);
mavContainer = new ModelAndViewContainer();
servletRequest = new MockHttpServletRequest();
servletResponse = new MockHttpServletResponse();
webRequest = new ServletWebRequest(servletRequest, servletResponse);
}
@Test
public void supportsParameter() {
assertTrue("RequestBody parameter not supported", processor.supportsParameter(paramRequestBodyString));
assertFalse("non-RequestBody parameter supported", processor.supportsParameter(paramInt));
}
@Test
public void supportsReturnType() {
assertTrue("ResponseBody return type not supported", processor.supportsReturnType(returnTypeString));
assertFalse("non-ResponseBody return type supported", processor.supportsReturnType(returnTypeInt));
}
@Test
public void resolveArgument() throws Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
String body = "Foo";
servletRequest.setContent(body.getBytes());
expect(messageConverter.canRead(String.class, contentType)).andReturn(true);
expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(body);
replay(messageConverter);
Object result = processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, new ValidatingBinderFactory());
assertEquals("Invalid argument", body, result);
assertFalse("The requestHandled flag shouldn't change", mavContainer.isRequestHandled());
verify(messageConverter);
}
@Test
public void resolveArgumentNotValid() throws Exception {
try {
testResolveArgumentWithValidation(new SimpleBean(null));
fail("Expected exception");
} catch (MethodArgumentNotValidException e) {
assertEquals("simpleBean", e.getBindingResult().getObjectName());
assertEquals(1, e.getBindingResult().getErrorCount());
assertNotNull(e.getBindingResult().getFieldError("name"));
}
}
@Test
public void resolveArgumentValid() throws Exception {
testResolveArgumentWithValidation(new SimpleBean("name"));
}
private void testResolveArgumentWithValidation(SimpleBean simpleBean) throws IOException, Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
servletRequest.setContent(new byte[] {});
@SuppressWarnings("unchecked")
HttpMessageConverter<SimpleBean> beanConverter = createMock(HttpMessageConverter.class);
expect(beanConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(beanConverter.canRead(SimpleBean.class, contentType)).andReturn(true);
expect(beanConverter.read(eq(SimpleBean.class), isA(HttpInputMessage.class))).andReturn(simpleBean);
replay(beanConverter);
processor = new RequestResponseBodyMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(beanConverter));
processor.resolveArgument(paramValidBean, mavContainer, webRequest, new ValidatingBinderFactory());
verify(beanConverter);
}
@Test(expected = HttpMediaTypeNotSupportedException.class)
public void resolveArgumentNotReadable() throws Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
servletRequest.setContent(new byte[] {});
expect(messageConverter.canRead(String.class, contentType)).andReturn(false);
replay(messageConverter);
processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null);
}
@Test(expected = HttpMediaTypeNotSupportedException.class)
public void resolveArgumentNoContentType() throws Exception {
servletRequest.setContent(new byte[] {});
processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null);
}
@Test(expected = HttpMessageNotReadableException.class)
public void resolveArgumentRequiredNoContent() throws Exception {
processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null);
}
@Test
public void resolveArgumentNotRequiredNoContent() throws Exception {
assertNull(processor.resolveArgument(paramStringNotRequired, mavContainer, webRequest, new ValidatingBinderFactory()));
}
@Test
public void handleReturnValue() throws Exception {
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
String body = "Foo";
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(true);
messageConverter.write(eq(body), eq(accepted), isA(HttpOutputMessage.class));
replay(messageConverter);
processor.handleReturnValue(body, returnTypeString, mavContainer, webRequest);
assertTrue("The requestHandled flag wasn't set", mavContainer.isRequestHandled());
verify(messageConverter);
}
@Test
public void handleReturnValueProduces() throws Exception {
String body = "Foo";
servletRequest.addHeader("Accept", "text/*");
servletRequest.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Collections.singleton(MediaType.TEXT_HTML));
expect(messageConverter.canWrite(String.class, MediaType.TEXT_HTML)).andReturn(true);
messageConverter.write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
replay(messageConverter);
processor.handleReturnValue(body, returnTypeStringProduces, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
verify(messageConverter);
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptable() throws Exception {
MediaType accepted = MediaType.APPLICATION_ATOM_XML;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
replay(messageConverter);
processor.handleReturnValue("Foo", returnTypeString, mavContainer, webRequest);
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptableProduces() throws Exception {
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
replay(messageConverter);
processor.handleReturnValue("Foo", returnTypeStringProduces, mavContainer, webRequest);
}
// SPR-9160
@Test
public void handleReturnValueSortByQuality() throws Exception {
this.servletRequest.addHeader("Accept", "text/plain; q=0.5, application/json");
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
converters.add(new StringHttpMessageConverter());
RequestResponseBodyMethodProcessor handler = new RequestResponseBodyMethodProcessor(converters);
handler.writeWithMessageConverters("Foo", returnTypeStringProduces, webRequest);
assertEquals("application/json;charset=UTF-8", servletResponse.getHeader("Content-Type"));
}
@Test
public void handleReturnValueString() throws Exception {
List<HttpMessageConverter<?>>converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new ByteArrayHttpMessageConverter());
converters.add(new StringHttpMessageConverter());
processor = new RequestResponseBodyMethodProcessor(converters);
processor.handleReturnValue("Foo", returnTypeString, mavContainer, webRequest);
assertEquals("text/plain;charset=ISO-8859-1", servletResponse.getHeader("Content-Type"));
assertEquals("Foo", servletResponse.getContentAsString());
}
@ResponseBody
public String handle1(@RequestBody String s, int i) {
return s;
}
public int handle2() {
return 42;
}
@ResponseBody
public String handle3() {
return null;
}
public void handle4(@Valid @RequestBody SimpleBean b) {
}
public void handle5(@RequestBody(required=false) String s) {
}
private final class ValidatingBinderFactory implements WebDataBinderFactory {
public WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
WebDataBinder dataBinder = new WebDataBinder(target, objectName);
dataBinder.setValidator(validator);
return dataBinder;
}
}
@SuppressWarnings("unused")
private static class SimpleBean {
@NotNull
private final String name;
public SimpleBean(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}

View File

@ -16,75 +16,43 @@
package org.springframework.web.servlet.mvc.method.annotation;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
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.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
/**
* Test fixture with {@link RequestResponseBodyMethodProcessor} and mock {@link HttpMessageConverter}.
* Test fixture for a {@link RequestResponseBodyMethodProcessor} with actual delegation
* to HttpMessageConverter instances.
*
* <p>Also see {@link RequestResponseBodyMethodProcessorMockTests}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
*/
public class RequestResponseBodyMethodProcessorTests {
private RequestResponseBodyMethodProcessor processor;
private HttpMessageConverter<String> messageConverter;
private MethodParameter paramRequestBodyString;
private MethodParameter paramInt;
private MethodParameter paramValidBean;
private MethodParameter paramStringNotRequired;
private MethodParameter paramGenericList;
private MethodParameter paramSimpleBean;
private MethodParameter returnTypeString;
private MethodParameter returnTypeInt;
private MethodParameter returnTypeStringProduces;
private ModelAndViewContainer mavContainer;
@ -94,24 +62,13 @@ public class RequestResponseBodyMethodProcessorTests {
private MockHttpServletResponse servletResponse;
@SuppressWarnings("unchecked")
@Before
public void setUp() throws Exception {
messageConverter = createMock(HttpMessageConverter.class);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
replay(messageConverter);
processor = new RequestResponseBodyMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(messageConverter));
reset(messageConverter);
Method handle = getClass().getMethod("handle1", String.class, Integer.TYPE);
paramRequestBodyString = new MethodParameter(handle, 0);
paramInt = new MethodParameter(handle, 1);
returnTypeString = new MethodParameter(handle, -1);
returnTypeInt = new MethodParameter(getClass().getMethod("handle2"), -1);
returnTypeStringProduces = new MethodParameter(getClass().getMethod("handle3"), -1);
paramValidBean = new MethodParameter(getClass().getMethod("handle4", SimpleBean.class), 0);
paramStringNotRequired = new MethodParameter(getClass().getMethod("handle5", String.class), 0);
Method method = getClass().getMethod("handle", List.class, SimpleBean.class);
paramGenericList = new MethodParameter(method, 0);
paramSimpleBean = new MethodParameter(method, 1);
returnTypeString = new MethodParameter(method, -1);
mavContainer = new ModelAndViewContainer();
@ -120,160 +77,41 @@ public class RequestResponseBodyMethodProcessorTests {
webRequest = new ServletWebRequest(servletRequest, servletResponse);
}
@Test
public void supportsParameter() {
assertTrue("RequestBody parameter not supported", processor.supportsParameter(paramRequestBodyString));
assertFalse("non-RequestBody parameter supported", processor.supportsParameter(paramInt));
}
@Test
public void supportsReturnType() {
assertTrue("ResponseBody return type not supported", processor.supportsReturnType(returnTypeString));
assertFalse("non-ResponseBody return type supported", processor.supportsReturnType(returnTypeInt));
public void resolveGenericArgument() throws Exception {
String content = "[{\"name\" : \"Jad\"}, {\"name\" : \"Robert\"}]";
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType("application/json");
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
@SuppressWarnings("unchecked")
List<SimpleBean> result = (List<SimpleBean>) processor.resolveArgument(
paramGenericList, mavContainer, webRequest, new ValidatingBinderFactory());
assertNotNull(result);
assertEquals("Jad", result.get(0).getName());
assertEquals("Robert", result.get(1).getName());
}
@Test
public void resolveArgument() throws Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
String content = "{\"name\" : \"Jad\"}";
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType("application/json");
String body = "Foo";
servletRequest.setContent(body.getBytes());
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
expect(messageConverter.canRead(String.class, contentType)).andReturn(true);
expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(body);
replay(messageConverter);
SimpleBean result = (SimpleBean) processor.resolveArgument(
paramSimpleBean, mavContainer, webRequest, new ValidatingBinderFactory());
Object result = processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, new ValidatingBinderFactory());
assertEquals("Invalid argument", body, result);
assertFalse("The requestHandled flag shouldn't change", mavContainer.isRequestHandled());
verify(messageConverter);
}
@Test
public void resolveArgumentNotValid() throws Exception {
try {
testResolveArgumentWithValidation(new SimpleBean(null));
fail("Expected exception");
} catch (MethodArgumentNotValidException e) {
assertEquals("simpleBean", e.getBindingResult().getObjectName());
assertEquals(1, e.getBindingResult().getErrorCount());
assertNotNull(e.getBindingResult().getFieldError("name"));
}
}
@Test
public void resolveArgumentValid() throws Exception {
testResolveArgumentWithValidation(new SimpleBean("name"));
}
private void testResolveArgumentWithValidation(SimpleBean simpleBean) throws IOException, Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
servletRequest.setContent(new byte[] {});
@SuppressWarnings("unchecked")
HttpMessageConverter<SimpleBean> beanConverter = createMock(HttpMessageConverter.class);
expect(beanConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(beanConverter.canRead(SimpleBean.class, contentType)).andReturn(true);
expect(beanConverter.read(eq(SimpleBean.class), isA(HttpInputMessage.class))).andReturn(simpleBean);
replay(beanConverter);
processor = new RequestResponseBodyMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(beanConverter));
processor.resolveArgument(paramValidBean, mavContainer, webRequest, new ValidatingBinderFactory());
verify(beanConverter);
}
@Test(expected = HttpMediaTypeNotSupportedException.class)
public void resolveArgumentNotReadable() throws Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
servletRequest.setContent(new byte[] {});
expect(messageConverter.canRead(String.class, contentType)).andReturn(false);
replay(messageConverter);
processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null);
}
@Test(expected = HttpMediaTypeNotSupportedException.class)
public void resolveArgumentNoContentType() throws Exception {
servletRequest.setContent(new byte[] {});
processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null);
}
@Test(expected = HttpMessageNotReadableException.class)
public void resolveArgumentRequiredNoContent() throws Exception {
processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null);
}
@Test
public void resolveArgumentNotRequiredNoContent() throws Exception {
assertNull(processor.resolveArgument(paramStringNotRequired, mavContainer, webRequest, new ValidatingBinderFactory()));
}
@Test
public void handleReturnValue() throws Exception {
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
String body = "Foo";
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(true);
messageConverter.write(eq(body), eq(accepted), isA(HttpOutputMessage.class));
replay(messageConverter);
processor.handleReturnValue(body, returnTypeString, mavContainer, webRequest);
assertTrue("The requestHandled flag wasn't set", mavContainer.isRequestHandled());
verify(messageConverter);
}
@Test
public void handleReturnValueProduces() throws Exception {
String body = "Foo";
servletRequest.addHeader("Accept", "text/*");
servletRequest.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, Collections.singleton(MediaType.TEXT_HTML));
expect(messageConverter.canWrite(String.class, MediaType.TEXT_HTML)).andReturn(true);
messageConverter.write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class));
replay(messageConverter);
processor.handleReturnValue(body, returnTypeStringProduces, mavContainer, webRequest);
assertTrue(mavContainer.isRequestHandled());
verify(messageConverter);
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptable() throws Exception {
MediaType accepted = MediaType.APPLICATION_ATOM_XML;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Arrays.asList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
replay(messageConverter);
processor.handleReturnValue("Foo", returnTypeString, mavContainer, webRequest);
}
@Test(expected = HttpMediaTypeNotAcceptableException.class)
public void handleReturnValueNotAcceptableProduces() throws Exception {
MediaType accepted = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Accept", accepted.toString());
expect(messageConverter.canWrite(String.class, null)).andReturn(true);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(messageConverter.canWrite(String.class, accepted)).andReturn(false);
replay(messageConverter);
processor.handleReturnValue("Foo", returnTypeStringProduces, mavContainer, webRequest);
assertNotNull(result);
assertEquals("Jad", result.getName());
}
// SPR-9160
@ -285,9 +123,9 @@ public class RequestResponseBodyMethodProcessorTests {
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
converters.add(new StringHttpMessageConverter());
RequestResponseBodyMethodProcessor handler = new RequestResponseBodyMethodProcessor(converters);
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
handler.writeWithMessageConverters("Foo", returnTypeStringProduces, webRequest);
processor.writeWithMessageConverters("Foo", returnTypeString, webRequest);
assertEquals("application/json;charset=UTF-8", servletResponse.getHeader("Content-Type"));
}
@ -298,31 +136,31 @@ public class RequestResponseBodyMethodProcessorTests {
converters.add(new ByteArrayHttpMessageConverter());
converters.add(new StringHttpMessageConverter());
processor = new RequestResponseBodyMethodProcessor(converters);
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
processor.handleReturnValue("Foo", returnTypeString, mavContainer, webRequest);
assertEquals("text/plain;charset=ISO-8859-1", servletResponse.getHeader("Content-Type"));
assertEquals("Foo", servletResponse.getContentAsString());
}
@ResponseBody
public String handle1(@RequestBody String s, int i) {
return s;
}
public int handle2() {
return 42;
}
@ResponseBody
public String handle3() {
public String handle(@RequestBody List<SimpleBean> list, @RequestBody SimpleBean simpleBean) {
return null;
}
public void handle4(@Valid @RequestBody SimpleBean b) {
}
public void handle5(@RequestBody(required=false) String s) {
private static class SimpleBean {
private String name;
public String getName() {
return name;
}
@SuppressWarnings("unused")
public void setName(String name) {
this.name = name;
}
}
private final class ValidatingBinderFactory implements WebDataBinderFactory {
@ -335,19 +173,4 @@ public class RequestResponseBodyMethodProcessorTests {
}
}
@SuppressWarnings("unused")
private static class SimpleBean {
@NotNull
private final String name;
public SimpleBean(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}