Add required flag to @RequestBody
If true and there is no body => HttpMessageNotReadableException If false and there is no body, the argument resolves to null. Issue: SPR-9239
This commit is contained in:
parent
0105c5ebb9
commit
77ae101402
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -44,4 +44,12 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||||
@Documented
|
@Documented
|
||||||
public @interface RequestBody {
|
public @interface RequestBody {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether body content is required.
|
||||||
|
* <p>Default is <code>true</code>, leading to an exception thrown in case
|
||||||
|
* there is no body content. Switch this to <code>false</code> if you prefer
|
||||||
|
* <code>null</value> to be passed when the body content is <code>null</code>.
|
||||||
|
*/
|
||||||
|
boolean required() default true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -23,9 +23,12 @@ import java.util.List;
|
||||||
import org.springframework.core.Conventions;
|
import org.springframework.core.Conventions;
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
|
import org.springframework.http.HttpInputMessage;
|
||||||
import org.springframework.http.converter.HttpMessageConverter;
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||||
|
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
@ -65,13 +68,32 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
|
||||||
return returnType.getMethodAnnotation(ResponseBody.class) != null;
|
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,
|
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
|
||||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
|
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
|
||||||
|
|
||||||
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getParameterType());
|
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();
|
Annotation[] annotations = parameter.getParameterAnnotations();
|
||||||
for (Annotation annot : annotations) {
|
for (Annotation annot : annotations) {
|
||||||
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
|
if (!annot.annotationType().getSimpleName().startsWith("Valid")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
String name = Conventions.getVariableNameForParameter(parameter);
|
String name = Conventions.getVariableNameForParameter(parameter);
|
||||||
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
|
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
|
||||||
Object hints = AnnotationUtils.getValue(annot);
|
Object hints = AnnotationUtils.getValue(annot);
|
||||||
|
@ -82,7 +104,20 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return arg;
|
|
||||||
|
@Override
|
||||||
|
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage,
|
||||||
|
MethodParameter methodParam, Class<T> 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,
|
public void handleReturnValue(Object returnValue, MethodParameter returnType,
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -26,6 +26,7 @@ import static org.easymock.EasyMock.verify;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
@ -47,6 +48,7 @@ import org.springframework.http.HttpOutputMessage;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||||
import org.springframework.http.converter.HttpMessageConverter;
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
@ -79,6 +81,7 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||||
private MethodParameter paramRequestBodyString;
|
private MethodParameter paramRequestBodyString;
|
||||||
private MethodParameter paramInt;
|
private MethodParameter paramInt;
|
||||||
private MethodParameter paramValidBean;
|
private MethodParameter paramValidBean;
|
||||||
|
private MethodParameter paramStringNotRequired;
|
||||||
private MethodParameter returnTypeString;
|
private MethodParameter returnTypeString;
|
||||||
private MethodParameter returnTypeInt;
|
private MethodParameter returnTypeInt;
|
||||||
private MethodParameter returnTypeStringProduces;
|
private MethodParameter returnTypeStringProduces;
|
||||||
|
@ -108,6 +111,7 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||||
returnTypeInt = new MethodParameter(getClass().getMethod("handle2"), -1);
|
returnTypeInt = new MethodParameter(getClass().getMethod("handle2"), -1);
|
||||||
returnTypeStringProduces = new MethodParameter(getClass().getMethod("handle3"), -1);
|
returnTypeStringProduces = new MethodParameter(getClass().getMethod("handle3"), -1);
|
||||||
paramValidBean = new MethodParameter(getClass().getMethod("handle4", SimpleBean.class), 0);
|
paramValidBean = new MethodParameter(getClass().getMethod("handle4", SimpleBean.class), 0);
|
||||||
|
paramStringNotRequired = new MethodParameter(getClass().getMethod("handle5", String.class), 0);
|
||||||
|
|
||||||
mavContainer = new ModelAndViewContainer();
|
mavContainer = new ModelAndViewContainer();
|
||||||
|
|
||||||
|
@ -134,6 +138,8 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||||
servletRequest.addHeader("Content-Type", contentType.toString());
|
servletRequest.addHeader("Content-Type", contentType.toString());
|
||||||
|
|
||||||
String body = "Foo";
|
String body = "Foo";
|
||||||
|
servletRequest.setContent(body.getBytes());
|
||||||
|
|
||||||
expect(messageConverter.canRead(String.class, contentType)).andReturn(true);
|
expect(messageConverter.canRead(String.class, contentType)).andReturn(true);
|
||||||
expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(body);
|
expect(messageConverter.read(eq(String.class), isA(HttpInputMessage.class))).andReturn(body);
|
||||||
replay(messageConverter);
|
replay(messageConverter);
|
||||||
|
@ -165,6 +171,7 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||||
private void testResolveArgumentWithValidation(SimpleBean simpleBean) throws IOException, Exception {
|
private void testResolveArgumentWithValidation(SimpleBean simpleBean) throws IOException, Exception {
|
||||||
MediaType contentType = MediaType.TEXT_PLAIN;
|
MediaType contentType = MediaType.TEXT_PLAIN;
|
||||||
servletRequest.addHeader("Content-Type", contentType.toString());
|
servletRequest.addHeader("Content-Type", contentType.toString());
|
||||||
|
servletRequest.setContent(new byte[] {});
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
HttpMessageConverter<SimpleBean> beanConverter = createMock(HttpMessageConverter.class);
|
HttpMessageConverter<SimpleBean> beanConverter = createMock(HttpMessageConverter.class);
|
||||||
|
@ -183,6 +190,7 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||||
public void resolveArgumentNotReadable() throws Exception {
|
public void resolveArgumentNotReadable() throws Exception {
|
||||||
MediaType contentType = MediaType.TEXT_PLAIN;
|
MediaType contentType = MediaType.TEXT_PLAIN;
|
||||||
servletRequest.addHeader("Content-Type", contentType.toString());
|
servletRequest.addHeader("Content-Type", contentType.toString());
|
||||||
|
servletRequest.setContent(new byte[] {});
|
||||||
|
|
||||||
expect(messageConverter.canRead(String.class, contentType)).andReturn(false);
|
expect(messageConverter.canRead(String.class, contentType)).andReturn(false);
|
||||||
replay(messageConverter);
|
replay(messageConverter);
|
||||||
|
@ -194,10 +202,22 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||||
|
|
||||||
@Test(expected = HttpMediaTypeNotSupportedException.class)
|
@Test(expected = HttpMediaTypeNotSupportedException.class)
|
||||||
public void resolveArgumentNoContentType() throws Exception {
|
public void resolveArgumentNoContentType() throws Exception {
|
||||||
|
servletRequest.setContent(new byte[] {});
|
||||||
processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null);
|
processor.resolveArgument(paramRequestBodyString, mavContainer, webRequest, null);
|
||||||
fail("Expected exception");
|
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
|
@Test
|
||||||
public void handleReturnValue() throws Exception {
|
public void handleReturnValue() throws Exception {
|
||||||
MediaType accepted = MediaType.TEXT_PLAIN;
|
MediaType accepted = MediaType.TEXT_PLAIN;
|
||||||
|
@ -310,6 +330,9 @@ public class RequestResponseBodyMethodProcessorTests {
|
||||||
public void handle4(@Valid @RequestBody SimpleBean b) {
|
public void handle4(@Valid @RequestBody SimpleBean b) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void handle5(@RequestBody(required=false) String s) {
|
||||||
|
}
|
||||||
|
|
||||||
private final class ValidatingBinderFactory implements WebDataBinderFactory {
|
private final class ValidatingBinderFactory implements WebDataBinderFactory {
|
||||||
public WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception {
|
public WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception {
|
||||||
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
|
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
|
||||||
|
|
|
@ -28,6 +28,7 @@ Changes in version 3.2 M1
|
||||||
* add ability to configure custom MessageCodesResolver through the MVC Java config
|
* add ability to configure custom MessageCodesResolver through the MVC Java config
|
||||||
* add option in MappingJacksonJsonView for setting the Content-Length header
|
* add option in MappingJacksonJsonView for setting the Content-Length header
|
||||||
* decode path variables when url decoding is turned off in AbstractHandlerMapping
|
* 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)
|
Changes in version 3.1.1 (2012-02-16)
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
Loading…
Reference in New Issue