diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java index b5712abed9b9..33bb41b8495d 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * 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. @@ -44,4 +44,12 @@ @Documented public @interface RequestBody { + /** + * Whether body content is required. + *

Default is true, leading to an exception thrown in case + * there is no body content. Switch this to false if you prefer + * null to be passed when the body content is null. + */ + boolean required() default true; + } 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 d2cd0e08a43a..722e85b6bca5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * 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. @@ -23,9 +23,12 @@ import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindingResult; 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; @@ -36,17 +39,17 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; /** - * Resolves method arguments annotated with {@code @RequestBody} and handles + * Resolves method arguments annotated with {@code @RequestBody} and handles * return values from methods annotated with {@code @ResponseBody} by reading - * and writing to the body of the request or response with an + * and writing to the body of the request or response with an * {@link HttpMessageConverter}. - * - *

An {@code @RequestBody} method argument is also validated if it is - * annotated with {@code @javax.validation.Valid}. In case of validation - * failure, {@link MethodArgumentNotValidException} is raised and results + * + *

An {@code @RequestBody} method argument is also validated if it is + * annotated with {@code @javax.validation.Valid}. In case of validation + * failure, {@link MethodArgumentNotValidException} is raised and results * in a 400 response status code if {@link DefaultHandlerExceptionResolver} - * is configured. - * + * is configured. + * * @author Arjen Poutsma * @author Rossen Stoyanchev * @since 3.1 @@ -65,24 +68,56 @@ public boolean supportsReturnType(MethodParameter returnType) { return returnType.getMethodAnnotation(ResponseBody.class) != null; } + /** + * {@inheritDoc} + * @throws MethodArgumentNotValidException if validation fails + * @throws HttpMessageNotReadableException if {@link RequestBody#required()} + * is {@code true} and there is no body content or if there is no suitable + * converter to read the content with. + */ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { Object arg = readWithMessageConverters(webRequest, parameter, parameter.getParameterType()); + validate(parameter, webRequest, binderFactory, arg); + return arg; + } + + private void validate(MethodParameter parameter, NativeWebRequest webRequest, + WebDataBinderFactory binderFactory, Object arg) throws Exception, MethodArgumentNotValidException { + + if (arg == null) { + return; + } Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation annot : annotations) { - if (annot.annotationType().getSimpleName().startsWith("Valid")) { - String name = Conventions.getVariableNameForParameter(parameter); - WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); - Object hints = AnnotationUtils.getValue(annot); - binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); - BindingResult bindingResult = binder.getBindingResult(); - if (bindingResult.hasErrors()) { - throw new MethodArgumentNotValidException(parameter, bindingResult); - } + if (!annot.annotationType().getSimpleName().startsWith("Valid")) { + continue; + } + String name = Conventions.getVariableNameForParameter(parameter); + WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); + Object hints = AnnotationUtils.getValue(annot); + binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + BindingResult bindingResult = binder.getBindingResult(); + if (bindingResult.hasErrors()) { + throw new MethodArgumentNotValidException(parameter, bindingResult); } } - return arg; + } + + @Override + protected Object readWithMessageConverters(HttpInputMessage inputMessage, + MethodParameter methodParam, Class paramType) throws IOException, HttpMediaTypeNotSupportedException { + + if (inputMessage.getBody() != null) { + return super.readWithMessageConverters(inputMessage, methodParam, paramType); + } + + RequestBody annot = methodParam.getParameterAnnotation(RequestBody.class); + if (!annot.required()) { + return null; + } + throw new HttpMessageNotReadableException("Required request body content is missing: " + methodParam.toString()); } public void handleReturnValue(Object returnValue, MethodParameter returnType, 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 8449f51e5efb..c3d252305248 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * 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. @@ -26,6 +26,7 @@ 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; @@ -47,6 +48,7 @@ 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; @@ -79,6 +81,7 @@ public class RequestResponseBodyMethodProcessorTests { private MethodParameter paramRequestBodyString; private MethodParameter paramInt; private MethodParameter paramValidBean; + private MethodParameter paramStringNotRequired; private MethodParameter returnTypeString; private MethodParameter returnTypeInt; private MethodParameter returnTypeStringProduces; @@ -108,6 +111,7 @@ public void setUp() throws Exception { 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(); @@ -134,6 +138,8 @@ public void resolveArgument() throws Exception { 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); @@ -165,6 +171,7 @@ public void resolveArgumentValid() throws Exception { 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); @@ -183,6 +190,7 @@ private void testResolveArgumentWithValidation(SimpleBean simpleBean) throws IOE 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); @@ -194,10 +202,22 @@ public void resolveArgumentNotReadable() throws Exception { @Test(expected = HttpMediaTypeNotSupportedException.class) public void resolveArgumentNoContentType() throws Exception { + servletRequest.setContent(new byte[] {}); + processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null); + fail("Expected exception"); + } + + @Test(expected = HttpMessageNotReadableException.class) + public void resolveArgumentRequiredNoContent() throws Exception { processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null); fail("Expected exception"); } + @Test + public void resolveArgumentNotRequiredNoContent() throws Exception { + assertNull(processor.resolveArgument(paramStringNotRequired, mavContainer, webRequest, null)); + } + @Test public void handleReturnValue() throws Exception { MediaType accepted = MediaType.TEXT_PLAIN; @@ -310,6 +330,9 @@ public String handle3() { 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(); diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index 5d271edffe37..9ced58bd9396 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -28,6 +28,7 @@ Changes in version 3.2 M1 * add ability to configure custom MessageCodesResolver through the MVC Java config * add option in MappingJacksonJsonView for setting the Content-Length header * decode path variables when url decoding is turned off in AbstractHandlerMapping +* add required flag to @RequestBody annotation Changes in version 3.1.1 (2012-02-16) -------------------------------------