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 9e1497016a0..fef30d66853 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,8 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.ArrayList; @@ -33,6 +35,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; @@ -57,6 +60,9 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; */ public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver { + private static final Object NO_VALUE = new Object(); + + protected final Log logger = LogFactory.getLog(getClass()); protected final List> messageConverters; @@ -135,9 +141,8 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements * @param the expected type of the argument value to be created * @param inputMessage the HTTP input message representing the current request * @param param the method parameter descriptor (may be {@code null}) - * @param targetType the type of object to create, not necessarily the same as - * the method parameter type (e.g. for {@code HttpEntity} method - * parameter the target type is String) + * @param targetType the target type, not necessarily the same as the method + * parameter type, e.g. for {@code HttpEntity}. * @return the created method argument value * @throws IOException if the reading from the request fails * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found @@ -165,6 +170,9 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements targetClass = (Class) resolvableType.resolve(); } + inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage); + Object body = NO_VALUE; + for (HttpMessageConverter converter : this.messageConverters) { Class> converterType = (Class>) converter.getClass(); if (converter instanceof GenericHttpMessageConverter) { @@ -173,9 +181,16 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements if (logger.isDebugEnabled()) { logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); } - inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType); - T body = (T) genericConverter.read(targetType, contextClass, inputMessage); - return getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType); + if (inputMessage.getBody() != null) { + inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType); + body = genericConverter.read(targetType, contextClass, inputMessage); + body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType); + } + else { + body = null; + body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType); + } + break; } } else if (targetClass != null) { @@ -183,14 +198,25 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements if (logger.isDebugEnabled()) { logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); } - inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType); - T body = ((HttpMessageConverter) converter).read(targetClass, inputMessage); - return getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType); + if (inputMessage.getBody() != null) { + inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType); + body = ((HttpMessageConverter) converter).read(targetClass, inputMessage); + body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType); + } + else { + body = null; + body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType); + } + break; } } } - throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes); + if (body == NO_VALUE) { + throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes); + } + + return body; } /** @@ -240,4 +266,47 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements return !hasBindingResult; } + + private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage { + + private final HttpHeaders headers; + + private final InputStream body; + + + public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException { + this.headers = inputMessage.getHeaders(); + InputStream inputStream = inputMessage.getBody(); + if (inputStream == null) { + this.body = null; + } + else if (inputStream.markSupported()) { + inputStream.mark(1); + this.body = (inputStream.read() != -1 ? inputStream : null); + inputStream.reset(); + } + else { + PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream); + int b = pushbackInputStream.read(); + if (b == -1) { + this.body = null; + } + else { + this.body = pushbackInputStream; + pushbackInputStream.unread(b); + } + } + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + + @Override + public InputStream getBody() throws IOException { + return this.body; + } + } + } 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 f6f55b274a0..38d38e02f9c 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 @@ -19,12 +19,14 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.net.URI; import java.util.List; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; @@ -120,8 +122,9 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro Object body = readWithMessageConverters(webRequest, parameter, paramType); if (RequestEntity.class.equals(parameter.getParameterType())) { - return new RequestEntity(body, inputMessage.getHeaders(), - inputMessage.getMethod(), inputMessage.getURI()); + URI url = inputMessage.getURI(); + HttpMethod httpMethod = inputMessage.getMethod(); + return new RequestEntity(body, inputMessage.getHeaders(), httpMethod, url); } else { return new HttpEntity(body, inputMessage.getHeaders()); 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 0fb8294cba2..c553bb7dbd8 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 @@ -17,8 +17,6 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; -import java.io.InputStream; -import java.io.PushbackInputStream; import java.lang.reflect.Type; import java.util.List; @@ -146,43 +144,14 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); HttpInputMessage inputMessage = new ServletServerHttpRequest(servletRequest); - InputStream inputStream = inputMessage.getBody(); - if (inputStream == null) { - return handleEmptyBody(methodParam); - } - else if (inputStream.markSupported()) { - inputStream.mark(1); - if (inputStream.read() == -1) { - return handleEmptyBody(methodParam); - } - inputStream.reset(); - } - else { - final PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream); - int b = pushbackInputStream.read(); - if (b == -1) { - return handleEmptyBody(methodParam); - } - else { - pushbackInputStream.unread(b); + Object arg = readWithMessageConverters(inputMessage, methodParam, paramType); + if (arg == null) { + if (methodParam.getParameterAnnotation(RequestBody.class).required()) { + throw new HttpMessageNotReadableException("Required request body is missing: " + methodParam); } - inputMessage = new ServletServerHttpRequest(servletRequest) { - @Override - public InputStream getBody() { - // Form POST should not get here - return pushbackInputStream; - } - }; } - return super.readWithMessageConverters(inputMessage, methodParam, paramType); - } - - private Object handleEmptyBody(MethodParameter param) { - if (param.getParameterAnnotation(RequestBody.class).required()) { - throw new HttpMessageNotReadableException("Required request body content is missing: " + param); - } - return null; + return arg; } @Override 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 index dc3e70b3c2a..3f8a2ce7f38 100644 --- 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 @@ -16,14 +16,19 @@ package org.springframework.web.servlet.mvc.method.annotation; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.*; +import static org.mockito.BDDMockito.isA; +import static org.mockito.Matchers.eq; +import static org.springframework.web.servlet.HandlerMapping.*; + import java.lang.reflect.Method; import java.net.URI; -import java.text.SimpleDateFormat; +import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; import org.junit.Before; import org.junit.Test; @@ -48,13 +53,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; -import static org.junit.Assert.*; -import static org.mockito.BDDMockito.any; -import static org.mockito.BDDMockito.*; -import static org.mockito.BDDMockito.isA; -import static org.mockito.Matchers.eq; -import static org.springframework.web.servlet.HandlerMapping.*; - /** * Test fixture for {@link HttpEntityMethodProcessor} delegating to a mock * {@link HttpMessageConverter}. @@ -136,10 +134,12 @@ public class HttpEntityMethodProcessorMockTests { @Test public void resolveArgument() throws Exception { + String body = "Foo"; + MediaType contentType = MediaType.TEXT_PLAIN; servletRequest.addHeader("Content-Type", contentType.toString()); + servletRequest.setContent(body.getBytes(Charset.forName("UTF-8"))); - String body = "Foo"; given(messageConverter.canRead(String.class, contentType)).willReturn(true); given(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).willReturn(body); @@ -152,14 +152,16 @@ public class HttpEntityMethodProcessorMockTests { @Test public void resolveArgumentRequestEntity() throws Exception { + String body = "Foo"; + MediaType contentType = MediaType.TEXT_PLAIN; servletRequest.addHeader("Content-Type", contentType.toString()); servletRequest.setMethod("GET"); servletRequest.setServerName("www.example.com"); servletRequest.setServerPort(80); servletRequest.setRequestURI("/path"); + servletRequest.setContent(body.getBytes(Charset.forName("UTF-8"))); - String body = "Foo"; given(messageConverter.canRead(String.class, contentType)).willReturn(true); given(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).willReturn(body); @@ -226,6 +228,7 @@ public class HttpEntityMethodProcessorMockTests { verify(messageConverter).write(eq(body), eq(MediaType.TEXT_HTML), isA(HttpOutputMessage.class)); } + @SuppressWarnings("unchecked") @Test public void handleReturnValueWithResponseBodyAdvice() throws Exception { ResponseEntity returnValue = new ResponseEntity<>(HttpStatus.OK); @@ -416,28 +419,35 @@ public class HttpEntityMethodProcessorMockTests { assertEquals(0, servletResponse.getContentAsByteArray().length); } - public ResponseEntity handle1(HttpEntity httpEntity, ResponseEntity responseEntity, int i, RequestEntity requestEntity) { - return responseEntity; + @SuppressWarnings("unused") + public ResponseEntity handle1(HttpEntity httpEntity, ResponseEntity entity, + int i, RequestEntity requestEntity) { + + return entity; } + @SuppressWarnings("unused") public HttpEntity handle2(HttpEntity entity) { return entity; } + @SuppressWarnings("unused") public CustomHttpEntity handle2x(HttpEntity entity) { return new CustomHttpEntity(); } + @SuppressWarnings("unused") public int handle3() { return 42; } + @SuppressWarnings("unused") @RequestMapping(produces = {"text/html", "application/xhtml+xml"}) public ResponseEntity handle4() { return null; } - + @SuppressWarnings("unused") public static class CustomHttpEntity extends HttpEntity { } 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 4be8fda147d..83d55a2aa32 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -16,9 +16,12 @@ package org.springframework.web.servlet.mvc.method.annotation; +import static org.junit.Assert.*; + import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.junit.Before; @@ -39,8 +42,6 @@ import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.ModelAndViewContainer; -import static org.junit.Assert.*; - /** * Test fixture with {@link HttpEntityMethodProcessor} delegating to * actual {@link HttpMessageConverter} instances. @@ -61,8 +62,6 @@ public class HttpEntityMethodProcessorTests { private MockHttpServletRequest servletRequest; - private MockHttpServletResponse servletResponse; - private ServletWebRequest webRequest; @@ -75,8 +74,7 @@ public class HttpEntityMethodProcessorTests { mavContainer = new ModelAndViewContainer(); binderFactory = new ValidatingBinderFactory(); servletRequest = new MockHttpServletRequest(); - servletResponse = new MockHttpServletResponse(); - webRequest = new ServletWebRequest(servletRequest, servletResponse); + webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse()); } @Test @@ -97,6 +95,23 @@ public class HttpEntityMethodProcessorTests { assertEquals("Jad", result.getBody().getName()); } + // SPR-12861 + + @Test + public void resolveArgumentWithEmptyBody() throws Exception { + this.servletRequest.setContent(new byte[0]); + this.servletRequest.setContentType("application/json"); + + List> converters = Arrays.asList(new MappingJackson2HttpMessageConverter()); + HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters); + + HttpEntity result = (HttpEntity) processor.resolveArgument(this.paramSimpleBean, + this.mavContainer, this.webRequest, this.binderFactory); + + assertNotNull(result); + assertNull(result.getBody()); + } + @Test public void resolveGenericArgument() throws Exception { String content = "[{\"name\" : \"Jad\"}, {\"name\" : \"Robert\"}]"; @@ -139,21 +154,21 @@ public class HttpEntityMethodProcessorTests { } + @SuppressWarnings("unused") public void handle(HttpEntity> arg1, HttpEntity arg2) { } - + @SuppressWarnings("unused") private static abstract class MyParameterizedController { public void handleDto(HttpEntity dto) { } } - + @SuppressWarnings("unused") private static class MySimpleParameterizedController extends MyParameterizedController { } - private interface Identifiable extends Serializable { public Long getId(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java index c69c14011e3..0b254d0099d 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -16,8 +16,13 @@ package org.springframework.web.servlet.mvc.method.annotation; +import static org.junit.Assert.*; + import java.net.URI; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; + import javax.servlet.MultipartConfigElement; import org.eclipse.jetty.server.Server; @@ -34,8 +39,10 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; @@ -57,8 +64,6 @@ import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; -import static org.junit.Assert.*; - /** * Test access to parts of a multipart request with {@link RequestPart}. * @@ -102,9 +107,16 @@ public class RequestPartIntegrationTests { @Before public void setUp() { + ByteArrayHttpMessageConverter emptyBodyConverter = new ByteArrayHttpMessageConverter(); + emptyBodyConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON)); + + List> converters = new ArrayList<>(3); + converters.add(emptyBodyConverter); + converters.add(new ResourceHttpMessageConverter()); + converters.add(new MappingJackson2HttpMessageConverter()); + AllEncompassingFormHttpMessageConverter converter = new AllEncompassingFormHttpMessageConverter(); - converter.setPartConverters(Arrays.>asList( - new ResourceHttpMessageConverter(), new MappingJackson2HttpMessageConverter())); + converter.setPartConverters(converters); restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); restTemplate.setMessageConverters(Arrays.>asList(converter)); @@ -130,9 +142,9 @@ public class RequestPartIntegrationTests { private void testCreate(String url) { MultiValueMap parts = new LinkedMultiValueMap(); - HttpEntity jsonEntity = new HttpEntity(new TestData("Jason")); - parts.add("json-data", jsonEntity); + parts.add("json-data", new HttpEntity(new TestData("Jason"))); parts.add("file-data", new ClassPathResource("logo.jpg", this.getClass())); + parts.add("empty-data", new HttpEntity(new byte[0])); // SPR-12860 URI location = restTemplate.postForLocation(url, parts); assertEquals("http://localhost:8080/test/Jason/logo.jpg", location.toString()); @@ -167,11 +179,15 @@ public class RequestPartIntegrationTests { } } + @SuppressWarnings("unused") @Controller private static class RequestPartTestController { @RequestMapping(value = "/test", method = RequestMethod.POST, consumes = { "multipart/mixed", "multipart/form-data" }) - public ResponseEntity create(@RequestPart("json-data") TestData testData, @RequestPart("file-data") MultipartFile file) { + public ResponseEntity create(@RequestPart("json-data") TestData testData, + @RequestPart("file-data") MultipartFile file, + @RequestPart(value = "empty-data", required = false) TestData emptyData) { + String url = "http://localhost:8080/test/" + testData.getName() + "/" + file.getOriginalFilename(); HttpHeaders headers = new HttpHeaders(); headers.setLocation(URI.create(url)); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java index a55c4ad90fb..d3a04537dd8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java @@ -16,11 +16,16 @@ package org.springframework.web.servlet.mvc.method.annotation; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; + import java.lang.reflect.Method; +import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; + import javax.servlet.http.Part; import javax.validation.Valid; import javax.validation.constraints.NotNull; @@ -31,6 +36,7 @@ import org.junit.Test; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.mock.web.test.MockHttpServletRequest; @@ -52,9 +58,6 @@ import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.MissingServletRequestPartException; import org.springframework.web.multipart.support.RequestPartServletServerHttpRequest; -import static org.junit.Assert.*; -import static org.mockito.BDDMockito.*; - /** * Test fixture with {@link RequestPartMethodArgumentResolver} and mock {@link HttpMessageConverter}. * @@ -90,8 +93,6 @@ public class RequestPartMethodArgumentResolverTests { private MockMultipartHttpServletRequest multipartRequest; - private MockHttpServletResponse servletResponse; - @SuppressWarnings("unchecked") @Before @@ -128,13 +129,14 @@ public class RequestPartMethodArgumentResolverTests { resolver = new RequestPartMethodArgumentResolver(Collections.>singletonList(messageConverter)); reset(messageConverter); - multipartFile1 = new MockMultipartFile("requestPart", "", "text/plain", (byte[]) null); - multipartFile2 = new MockMultipartFile("requestPart", "", "text/plain", (byte[]) null); + byte[] content = "doesn't matter as long as not empty".getBytes(Charset.forName("UTF-8")); + + multipartFile1 = new MockMultipartFile("requestPart", "", "text/plain", content); + multipartFile2 = new MockMultipartFile("requestPart", "", "text/plain", content); multipartRequest = new MockMultipartHttpServletRequest(); multipartRequest.addFile(multipartFile1); multipartRequest.addFile(multipartFile2); - servletResponse = new MockHttpServletResponse(); - webRequest = new ServletWebRequest(multipartRequest, servletResponse); + webRequest = new ServletWebRequest(multipartRequest, new MockHttpServletResponse()); } @@ -361,7 +363,7 @@ public class RequestPartMethodArgumentResolverTests { SimpleBean simpleBean = new SimpleBean("foo"); given(messageConverter.canRead(SimpleBean.class, MediaType.TEXT_PLAIN)).willReturn(true); - given(messageConverter.read(eq(SimpleBean.class), isA(RequestPartServletServerHttpRequest.class))).willReturn(simpleBean); + given(messageConverter.read(eq(SimpleBean.class), isA(HttpInputMessage.class))).willReturn(simpleBean); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); Object actualValue = resolver.resolveArgument(optionalRequestPart, mavContainer, webRequest, new ValidatingBinderFactory()); @@ -385,7 +387,7 @@ public class RequestPartMethodArgumentResolverTests { private void testResolveArgument(SimpleBean argValue, MethodParameter parameter) throws Exception { given(messageConverter.canRead(SimpleBean.class, MediaType.TEXT_PLAIN)).willReturn(true); - given(messageConverter.read(eq(SimpleBean.class), isA(RequestPartServletServerHttpRequest.class))).willReturn(argValue); + given(messageConverter.read(eq(SimpleBean.class), isA(HttpInputMessage.class))).willReturn(argValue); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); Object actualValue = resolver.resolveArgument(parameter, mavContainer, webRequest, new ValidatingBinderFactory()); @@ -423,7 +425,7 @@ public class RequestPartMethodArgumentResolverTests { } } - + @SuppressWarnings("unused") public void handle(@RequestPart SimpleBean requestPart, @RequestPart(value="requestPart", required=false) SimpleBean namedRequestPart, @Valid @RequestPart("requestPart") SimpleBean validRequestPart, 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 index 59ed692a13c..543030050c8 100644 --- 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 @@ -16,12 +16,15 @@ package org.springframework.web.servlet.mvc.method.annotation; -import java.io.IOException; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; + import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collections; import java.util.List; + import javax.validation.Valid; import javax.validation.constraints.NotNull; @@ -49,9 +52,6 @@ import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.HandlerMapping; -import static org.junit.Assert.*; -import static org.mockito.BDDMockito.*; - /** * Test fixture for {@link RequestResponseBodyMethodProcessor} delegating to a * mock HttpMessageConverter. @@ -81,7 +81,6 @@ public class RequestResponseBodyMethodProcessorMockTests { private MockHttpServletRequest servletRequest; - private MockHttpServletResponse servletResponse; @SuppressWarnings("unchecked") @Before @@ -103,8 +102,7 @@ public class RequestResponseBodyMethodProcessorMockTests { mavContainer = new ModelAndViewContainer(); servletRequest = new MockHttpServletRequest(); - servletResponse = new MockHttpServletResponse(); - webRequest = new ServletWebRequest(servletRequest, servletResponse); + webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse()); } @Test @@ -154,7 +152,7 @@ public class RequestResponseBodyMethodProcessorMockTests { testResolveArgumentWithValidation(new SimpleBean("name")); } - private void testResolveArgumentWithValidation(SimpleBean simpleBean) throws IOException, Exception { + private void testResolveArgumentWithValidation(SimpleBean simpleBean) throws Exception { MediaType contentType = MediaType.TEXT_PLAIN; servletRequest.addHeader("Content-Type", contentType.toString()); servletRequest.setContent("payload".getBytes(Charset.forName("UTF-8"))); @@ -189,6 +187,7 @@ public class RequestResponseBodyMethodProcessorMockTests { fail("Expected exception"); } catch (HttpMediaTypeNotSupportedException ex) { + // expected } } @@ -212,7 +211,9 @@ public class RequestResponseBodyMethodProcessorMockTests { @Test public void resolveArgumentNotRequiredNoContent() throws Exception { + servletRequest.setContentType("text/plain"); servletRequest.setContent(new byte[0]); + given(messageConverter.canRead(String.class, MediaType.TEXT_PLAIN)).willReturn(true); assertNull(processor.resolveArgument(paramStringNotRequired, mavContainer, webRequest, new ValidatingBinderFactory())); } @@ -293,23 +294,28 @@ public class RequestResponseBodyMethodProcessorMockTests { } + @SuppressWarnings("unused") @ResponseBody public String handle1(@RequestBody String s, int i) { return s; } + @SuppressWarnings("unused") public int handle2() { return 42; } + @SuppressWarnings("unused") @ResponseBody public String handle3() { return null; } + @SuppressWarnings("unused") public void handle4(@Valid @RequestBody SimpleBean b) { } + @SuppressWarnings("unused") public void handle5(@RequestBody(required=false) String s) { } 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 b98ab25b939..2e64b0a5fe6 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 @@ -18,6 +18,7 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.Serializable; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -29,6 +30,7 @@ import org.junit.Test; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.target.SingletonTargetSource; import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -177,7 +179,9 @@ public class RequestResponseBodyMethodProcessorTests { assertEquals("foobarbaz", result); } - @Test(expected = HttpMessageNotReadableException.class) // SPR-9942 + // SPR-9942 + + @Test(expected = HttpMessageNotReadableException.class) public void resolveArgumentRequiredNoContent() throws Exception { this.servletRequest.setContent(new byte[0]); this.servletRequest.setContentType("text/plain"); @@ -187,7 +191,23 @@ public class RequestResponseBodyMethodProcessorTests { processor.resolveArgument(paramString, mavContainer, webRequest, binderFactory); } - @Test // SPR-9964 + // SPR-12778 + + @Test + public void resolveArgumentRequiredNoContentDefaultValue() throws Exception { + this.servletRequest.setContent(new byte[0]); + this.servletRequest.setContentType("text/plain"); + List> converters = Arrays.asList(new StringHttpMessageConverter()); + List advice = Arrays.asList(new EmptyRequestBodyAdvice()); + RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters, advice); + String arg = (String) processor.resolveArgument(paramString, mavContainer, webRequest, binderFactory); + assertNotNull(arg); + assertEquals("default value for empty body", arg); + } + + // SPR-9964 + + @Test public void resolveArgumentTypeVariable() throws Exception { Method method = MyParameterizedController.class.getMethod("handleDto", Identifiable.class); HandlerMethod handlerMethod = new HandlerMethod(new MySimpleParameterizedController(), method); @@ -207,7 +227,9 @@ public class RequestResponseBodyMethodProcessorTests { assertEquals("Jad", result.getName()); } - @Test // SPR-11225 + // SPR-11225 + + @Test public void resolveArgumentTypeVariableWithNonGenericConverter() throws Exception { Method method = MyParameterizedController.class.getMethod("handleDto", Identifiable.class); HandlerMethod handlerMethod = new HandlerMethod(new MySimpleParameterizedController(), method); @@ -229,7 +251,9 @@ public class RequestResponseBodyMethodProcessorTests { assertEquals("Jad", result.getName()); } - @Test // SPR-9160 + // SPR-9160 + + @Test public void handleReturnValueSortByQuality() throws Exception { this.servletRequest.addHeader("Accept", "text/plain; q=0.5, application/json"); @@ -383,6 +407,7 @@ public class RequestResponseBodyMethodProcessorTests { } + @SuppressWarnings("unused") public String handle( @RequestBody List list, @RequestBody SimpleBean simpleBean, @@ -393,6 +418,7 @@ public class RequestResponseBodyMethodProcessorTests { } + @SuppressWarnings("unused") private static abstract class MyParameterizedController { @SuppressWarnings("unused") @@ -400,6 +426,7 @@ public class RequestResponseBodyMethodProcessorTests { } + @SuppressWarnings("unused") private static class MySimpleParameterizedController extends MyParameterizedController { } @@ -472,9 +499,10 @@ public class RequestResponseBodyMethodProcessorTests { } } - private interface MyJacksonView1 {}; - private interface MyJacksonView2 {}; + private interface MyJacksonView1 {} + private interface MyJacksonView2 {} + @SuppressWarnings("unused") private static class JacksonViewBean { @JsonView(MyJacksonView1.class) @@ -537,4 +565,35 @@ public class RequestResponseBodyMethodProcessorTests { } + private static class EmptyRequestBodyAdvice implements RequestBodyAdvice { + + @Override + public boolean supports(MethodParameter methodParameter, Type targetType, + Class> converterType) { + + return StringHttpMessageConverter.class.equals(converterType); + } + + @Override + public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType) { + + return "default value for empty body"; + } + + @Override + public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType) { + + return inputMessage; + } + + @Override + public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType) { + + return body; + } + } + }