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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user