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 dd9de11d6d9..7fd2dd7b108 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 @@ -42,6 +42,8 @@ public class HttpMessageConverterExtractor implements ResponseExtractor { private final Type responseType; + private final Class responseClass; + private final List> messageConverters; private final Log logger; @@ -64,10 +66,12 @@ public class HttpMessageConverterExtractor implements ResponseExtractor { this(responseType, messageConverters, LogFactory.getLog(HttpMessageConverterExtractor.class)); } + @SuppressWarnings("unchecked") HttpMessageConverterExtractor(Type responseType, List> 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) responseType : null; this.messageConverters = messageConverters; this.logger = logger; } @@ -79,21 +83,8 @@ public class HttpMessageConverterExtractor implements ResponseExtractor { } MediaType contentType = getContentType(response); - Class 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 implements ResponseExtractor { 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 [" + 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 3450cf6f931..3c0fc2f0bfa 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 @@ -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 Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam, Class paramType) throws IOException, - HttpMediaTypeNotSupportedException { + protected 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 Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter methodParam, - Class paramType) throws IOException, HttpMediaTypeNotSupportedException { + Type paramType) throws IOException, HttpMediaTypeNotSupportedException { MediaType contentType = inputMessage.getHeaders().getContentType(); if (contentType == null) { contentType = MediaType.APPLICATION_OCTET_STREAM; } + Class 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) messageConverter).read(paramClass, inputMessage); } - return ((HttpMessageConverter) messageConverter).read(paramType, inputMessage); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index 2be7e00b80d..a600d0aa87e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -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(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"); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java index 06e321ad102..c6569b77fa1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java @@ -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 Object readWithMessageConverters(HttpInputMessage inputMessage, - MethodParameter methodParam, Class paramType) throws IOException, HttpMediaTypeNotSupportedException { + MethodParameter methodParam, Type paramType) throws IOException, HttpMediaTypeNotSupportedException { if (inputMessage.getBody() != null) { return super.readWithMessageConverters(inputMessage, methodParam, paramType); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java new file mode 100644 index 00000000000..4d0a9ad29d7 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorMockTests.java @@ -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}. + * + *

Also see {@link HttpEntityMethodProcessorTests}. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + */ +public class HttpEntityMethodProcessorMockTests { + + private HttpEntityMethodProcessor processor; + + private HttpMessageConverter 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.>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 returnValue = new ResponseEntity(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 returnValue = new ResponseEntity(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 returnValue = new ResponseEntity(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 returnValue = new ResponseEntity(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 returnValue = new ResponseEntity("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 returnValue = new ResponseEntity(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 returnValue = new ResponseEntity("body", responseHeaders, HttpStatus.ACCEPTED); + + Capture outputMessage = new Capture(); + 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 handle1(HttpEntity httpEntity, ResponseEntity 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 handle4() { + return null; + } + + +} \ No newline at end of file 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 6c8e7a7a659..4660c44f6c5 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 @@ -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. + * + *

Also see {@link HttpEntityMethodProcessorMockTests}. * - * @author Arjen Poutsma * @author Rossen Stoyanchev */ public class HttpEntityMethodProcessorTests { - private HttpEntityMethodProcessor processor; - - private HttpMessageConverter 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.>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> converters = new ArrayList>(); + converters.add(new MappingJackson2HttpMessageConverter()); + HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters); - Object result = processor.resolveArgument(paramHttpEntity, mavContainer, webRequest, null); + @SuppressWarnings("unchecked") + HttpEntity result = (HttpEntity) 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 returnValue = new ResponseEntity(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> converters = new ArrayList>(); + 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> result = (HttpEntity>) 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 returnValue = new ResponseEntity(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> arg1, HttpEntity arg2) { } - @Test(expected = HttpMediaTypeNotAcceptableException.class) - public void handleReturnValueNotAcceptable() throws Exception { - String body = "Foo"; - ResponseEntity returnValue = new ResponseEntity(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 returnValue = new ResponseEntity(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 returnValue = new ResponseEntity("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 returnValue = new ResponseEntity(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 returnValue = new ResponseEntity("body", responseHeaders, HttpStatus.ACCEPTED); - - Capture outputMessage = new Capture(); - 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 handle1(HttpEntity httpEntity, ResponseEntity 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 handle4() { - return null; - } - - } \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorMockTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorMockTests.java new file mode 100644 index 00000000000..5db3b6d96f7 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorMockTests.java @@ -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. + * + *

Also see {@link RequestResponseBodyMethodProcessorTests}. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + */ +public class RequestResponseBodyMethodProcessorMockTests { + + private RequestResponseBodyMethodProcessor processor; + + private HttpMessageConverter 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.>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 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.>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> converters = new ArrayList>(); + 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>converters = new ArrayList>(); + 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; + } + } + +} \ No newline at end of file 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 4696fd87eb0..1a4726e5b09 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 @@ -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. + * + *

Also see {@link RequestResponseBodyMethodProcessorMockTests}. * - * @author Arjen Poutsma * @author Rossen Stoyanchev */ public class RequestResponseBodyMethodProcessorTests { - private RequestResponseBodyMethodProcessor processor; - - private HttpMessageConverter 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.>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> converters = new ArrayList>(); + converters.add(new MappingJackson2HttpMessageConverter()); + RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); + + @SuppressWarnings("unchecked") + List result = (List) 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> converters = new ArrayList>(); + 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 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.>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> converters = new ArrayList>(); 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 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; - } - } - } \ No newline at end of file