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)
-------------------------------------