Allow Errors after @RequestBody and @RequestPart

An @RequestBody or an @RequestPart argument can now be followed by an
Errors/BindingResult argument making it possible to handle validation
errors (as a result of an @Valid annotation) locally within the
@RequestMapping method.

Issue: SPR-7114
This commit is contained in:
Rossen Stoyanchev
2012-08-21 14:23:51 -04:00
parent 06d95915a0
commit af1561634c
9 changed files with 196 additions and 112 deletions

View File

@@ -38,8 +38,8 @@ import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
/**
* A base class for resolving method argument values by reading from the body of a request
* with {@link HttpMessageConverter}s.
* A base class for resolving method argument values by reading from the body of
* a request with {@link HttpMessageConverter}s.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
@@ -48,9 +48,9 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver;
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
protected final Log logger = LogFactory.getLog(getClass());
protected final List<HttpMessageConverter<?>> messageConverters;
protected final List<MediaType> allSupportedMediaTypes;
public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) {
@@ -60,8 +60,8 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
}
/**
* Returns the media types supported by all provided message converters preserving their ordering and
* further sorting by specificity via {@link MediaType#sortBySpecificity(List)}.
* Return the media types supported by all provided message converters sorted
* by specificity via {@link MediaType#sortBySpecificity(List)}.
*/
private static List<MediaType> getAllSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) {
Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<MediaType>();
@@ -72,11 +72,12 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
MediaType.sortBySpecificity(result);
return Collections.unmodifiableList(result);
}
/**
* Creates the method argument value of the expected parameter type by reading from the given request.
*
* @param <T> the expected type of the argument value to be created
* Creates the method argument value of the expected parameter type by
* reading from the given request.
*
* @param <T> the expected type of the argument value to be created
* @param webRequest the current request
* @param methodParam the method argument
* @param paramType the type of the argument value to be created
@@ -86,15 +87,16 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
*/
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam, Class<T> paramType) throws IOException,
HttpMediaTypeNotSupportedException {
HttpInputMessage inputMessage = createInputMessage(webRequest);
return readWithMessageConverters(inputMessage, methodParam, paramType);
}
HttpInputMessage inputMessage = createInputMessage(webRequest);
return readWithMessageConverters(inputMessage, methodParam, paramType);
}
/**
* Creates the method argument value of the expected parameter type by reading from the given HttpInputMessage.
*
* @param <T> the expected type of the argument value to be created
* Creates the method argument value of the expected parameter type by reading
* from the given HttpInputMessage.
*
* @param <T> the expected type of the argument value to be created
* @param inputMessage the HTTP input message representing the current request
* @param methodParam the method argument
* @param paramType the type of the argument value to be created
@@ -103,14 +105,14 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
*/
@SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter methodParam, Class<T> paramType) throws IOException,
HttpMediaTypeNotSupportedException {
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter methodParam,
Class<T> paramType) throws IOException, HttpMediaTypeNotSupportedException {
MediaType contentType = inputMessage.getHeaders().getContentType();
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter.canRead(paramType, contentType)) {
if (logger.isDebugEnabled()) {
@@ -120,7 +122,7 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
return ((HttpMessageConverter<T>) messageConverter).read(paramType, inputMessage);
}
}
throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes);
}

View File

@@ -28,6 +28,7 @@ import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestBody;
@@ -49,26 +50,26 @@ import org.springframework.web.util.WebUtils;
* Resolves the following method arguments:
* <ul>
* <li>Annotated with {@code @RequestPart}
* <li>Of type {@link MultipartFile} in conjunction with Spring's
* <li>Of type {@link MultipartFile} in conjunction with Spring's
* {@link MultipartResolver} abstraction
* <li>Of type {@code javax.servlet.http.Part} in conjunction with
* <li>Of type {@code javax.servlet.http.Part} in conjunction with
* Servlet 3.0 multipart requests
* </ul>
*
* <p>When a parameter is annotated with {@code @RequestPart} the content of the
* part is passed through an {@link HttpMessageConverter} to resolve the method
* argument with the 'Content-Type' of the request part in mind. This is
*
* <p>When a parameter is annotated with {@code @RequestPart} the content of the
* part is passed through an {@link HttpMessageConverter} to resolve the method
* argument with the 'Content-Type' of the request part in mind. This is
* analogous to what @{@link RequestBody} does to resolve an argument based on
* the content of a regular request.
*
* <p>When a parameter is not annotated or the name of the part is not specified,
*
* <p>When a parameter is not annotated or the name of the part is not specified,
* it is derived from the name of the method argument.
*
* <p>Automatic validation may be applied if the argument is annotated with
* {@code @javax.validation.Valid}. In case of validation failure, a
*
* <p>Automatic validation may be applied if the argument is annotated with
* {@code @javax.validation.Valid}. In case of validation failure, a
* {@link MethodArgumentNotValidException} is raised and a 400 response status
* code returned if {@link DefaultHandlerExceptionResolver} is configured.
*
*
* @author Rossen Stoyanchev
* @since 3.1
*/
@@ -111,8 +112,8 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
assertIsMultipartRequest(servletRequest);
MultipartHttpServletRequest multipartRequest =
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
String partName = getPartName(parameter);
@@ -133,20 +134,11 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
try {
HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, partName);
arg = readWithMessageConverters(inputMessage, parameter, parameter.getParameterType());
WebDataBinder binder = binderFactory.createBinder(request, arg, partName);
if (arg != null) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) {
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
WebDataBinder binder = binderFactory.createBinder(request, arg, partName);
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);
}
}
}
validate(binder, parameter);
}
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + partName, binder.getBindingResult());
}
catch (MissingServletRequestPartException ex) {
// handled below
@@ -160,7 +152,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
if (arg == null && isRequired) {
throw new MissingServletRequestPartException(partName);
}
return arg;
}
@@ -170,7 +162,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
throw new MultipartException("The current request is not a multipart request");
}
}
private String getPartName(MethodParameter parameter) {
RequestPart annot = parameter.getParameterAnnotation(RequestPart.class);
String partName = (annot != null) ? annot.value() : "";
@@ -181,7 +173,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
}
return partName;
}
private boolean isMultipartFileCollection(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
if (Collection.class.equals(paramType) || List.class.isAssignableFrom(paramType)){
@@ -193,4 +185,35 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
return false;
}
private void validate(WebDataBinder binder, MethodParameter parameter) throws MethodArgumentNotValidException {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) {
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(annot);
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.hasErrors()) {
if (isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, bindingResult);
}
}
}
}
}
/**
* Whether to raise a {@link MethodArgumentNotValidException} on validation errors.
* @param binder the data binder used to perform data binding
* @param parameter the method argument
* @return {@code true} if the next method argument is not of type {@link Errors}.
*/
private boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
int i = parameter.getParameterIndex();
Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
return !hasBindingResult;
}
}

View File

@@ -27,6 +27,7 @@ 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.validation.Errors;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.accept.ContentNegotiationManager;
@@ -85,33 +86,52 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
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;
Object argument = readWithMessageConverters(webRequest, parameter, parameter.getParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, argument, name);
if (argument != null) {
validate(binder, parameter);
}
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
return argument;
}
private void validate(MethodParameter parameter, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory, Object arg) throws Exception, MethodArgumentNotValidException {
private void validate(WebDataBinder binder, MethodParameter parameter) throws Exception, MethodArgumentNotValidException {
if (arg == null) {
return;
}
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) {
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);
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(annot);
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.hasErrors()) {
if (isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, bindingResult);
}
}
break;
}
}
}
/**
* Whether to raise a {@link MethodArgumentNotValidException} on validation errors.
* @param binder the data binder used to perform data binding
* @param parameter the method argument
* @return {@code true} if the next method argument is not of type {@link Errors}.
*/
private boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
int i = parameter.getParameterIndex();
Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
return !hasBindingResult;
}
@Override
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage,
MethodParameter methodParam, Class<T> paramType) throws IOException, HttpMediaTypeNotSupportedException {