SPR-8483 Add support for @RequestPart annotated method parameters

This commit is contained in:
Rossen Stoyanchev 2011-06-28 19:22:33 +00:00
parent 3bbefb3e65
commit 3a87d8e7cb
23 changed files with 912 additions and 114 deletions

View File

@ -84,6 +84,7 @@ import org.springframework.web.servlet.mvc.method.annotation.support.DefaultMeth
import org.springframework.web.servlet.mvc.method.annotation.support.HttpEntityMethodProcessor; import org.springframework.web.servlet.mvc.method.annotation.support.HttpEntityMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.support.ModelAndViewMethodReturnValueHandler; import org.springframework.web.servlet.mvc.method.annotation.support.ModelAndViewMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.method.annotation.support.PathVariableMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.support.PathVariableMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.support.RequestPartMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.support.RequestResponseBodyMethodProcessor; import org.springframework.web.servlet.mvc.method.annotation.support.RequestResponseBodyMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.support.ServletCookieValueMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.support.ServletCookieValueMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.support.ServletModelAttributeMethodProcessor; import org.springframework.web.servlet.mvc.method.annotation.support.ServletModelAttributeMethodProcessor;
@ -352,6 +353,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i
argumentResolvers.addResolver(new PathVariableMethodArgumentResolver()); argumentResolvers.addResolver(new PathVariableMethodArgumentResolver());
argumentResolvers.addResolver(new ServletModelAttributeMethodProcessor(false)); argumentResolvers.addResolver(new ServletModelAttributeMethodProcessor(false));
argumentResolvers.addResolver(new RequestResponseBodyMethodProcessor(messageConverters)); argumentResolvers.addResolver(new RequestResponseBodyMethodProcessor(messageConverters));
argumentResolvers.addResolver(new RequestPartMethodArgumentResolver(messageConverters));
argumentResolvers.addResolver(new RequestHeaderMethodArgumentResolver(beanFactory)); argumentResolvers.addResolver(new RequestHeaderMethodArgumentResolver(beanFactory));
argumentResolvers.addResolver(new RequestHeaderMapMethodArgumentResolver()); argumentResolvers.addResolver(new RequestHeaderMapMethodArgumentResolver());
argumentResolvers.addResolver(new ServletCookieValueMethodArgumentResolver(beanFactory)); argumentResolvers.addResolver(new ServletCookieValueMethodArgumentResolver(beanFactory));

View File

@ -0,0 +1,137 @@
/*
* Copyright 2002-2011 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation.support;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.web.HttpMediaTypeNotSupportedException;
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.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1
*/
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) {
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.messageConverters = messageConverters;
this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters);
}
/**
* Returns the media types supported by all provided message converters preserving their ordering and
* further sorting by specificity via {@link MediaType#sortBySpecificity(List)}.
*/
private static List<MediaType> getAllSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) {
Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<MediaType>();
for (HttpMessageConverter<?> messageConverter : messageConverters) {
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
}
List<MediaType> result = new ArrayList<MediaType>(allSupportedMediaTypes);
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
* @param webRequest the current request
* @param methodParam the method argument
* @param paramType the type of the argument value to be created
* @return the created method argument value
* @throws IOException if the reading from the request fails
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
*/
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam, Class<T> paramType) throws IOException,
HttpMediaTypeNotSupportedException {
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
* @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
* @return the created method argument value
* @throws IOException if the reading from the request fails
* @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 {
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()) {
logger.debug("Reading [" + paramType.getName() + "] as \"" + contentType + "\" using [" +
messageConverter + "]");
}
return ((HttpMessageConverter<T>) messageConverter).read(paramType, inputMessage);
}
}
throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes);
}
/**
* Creates a new {@link HttpInputMessage} from the given {@link NativeWebRequest}.
*
* @param webRequest the web request to create an input message from
* @return the input message
*/
protected ServletServerHttpRequest createInputMessage(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return new ServletServerHttpRequest(servletRequest);
}
}

View File

@ -27,8 +27,6 @@ import java.util.Set;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage; import org.springframework.http.HttpOutputMessage;
@ -36,90 +34,27 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.HandlerMapping;
/** /**
* A base class for resolving method argument values by reading from the body of a request with {@link * Extends {@link AbstractMessageConverterMethodArgumentResolver} with the ability to handle method return
* HttpMessageConverter}s and for handling method return values by writing to the response with {@link * values by writing to the response with {@link HttpMessageConverter}s.
* HttpMessageConverter}s.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.1 * @since 3.1
*/ */
public abstract class AbstractMessageConverterMethodProcessor public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { implements HandlerMethodReturnValueHandler {
private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application"); private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application");
protected final Log logger = LogFactory.getLog(getClass());
private final List<HttpMessageConverter<?>> messageConverters;
private final List<MediaType> allSupportedMediaTypes;
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters) { protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); super(messageConverters);
this.messageConverters = messageConverters;
this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters);
}
/**
* Returns the media types supported by all provided message converters preserving their ordering and
* further sorting by specificity via {@link MediaType#sortBySpecificity(List)}.
*/
private static List<MediaType> getAllSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) {
Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<MediaType>();
for (HttpMessageConverter<?> messageConverter : messageConverters) {
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
}
List<MediaType> result = new ArrayList<MediaType>(allSupportedMediaTypes);
MediaType.sortBySpecificity(result);
return Collections.unmodifiableList(result);
}
@SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest,
MethodParameter methodParam,
Class<T> paramType)
throws IOException, HttpMediaTypeNotSupportedException {
HttpInputMessage inputMessage = createInputMessage(webRequest);
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()) {
logger.debug("Reading [" + paramType.getName() + "] as \"" + contentType + "\" using [" +
messageConverter + "]");
}
return ((HttpMessageConverter<T>) messageConverter).read(paramType, inputMessage);
}
}
throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes);
}
/**
* Creates a new {@link HttpInputMessage} from the given {@link NativeWebRequest}.
*
* @param webRequest the web request to create an input message from
* @return the input message
*/
protected ServletServerHttpRequest createInputMessage(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return new ServletServerHttpRequest(servletRequest);
} }
/** /**

View File

@ -0,0 +1,124 @@
/*
* Copyright 2002-2011 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation.support;
import java.lang.annotation.Annotation;
import java.util.List;
import javax.servlet.ServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartRequest;
import org.springframework.web.multipart.RequestPartServletServerHttpRequest;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
import org.springframework.web.util.WebUtils;
/**
* Resolves method arguments annotated with @{@link RequestPart} expecting the request to be a
* {@link MultipartHttpServletRequest} and binding the method argument to a specific part of the multipart request.
* The name of the part is derived either from the {@link RequestPart} annotation or from the name of the method
* argument as a fallback.
*
* <p>An @{@link RequestPart} method argument will be validated if annotated with {@code @Valid}. In case of
* validation failure, a {@link RequestPartNotValidException} is thrown and can be handled automatically through
* the {@link DefaultHandlerExceptionResolver}. A {@link Validator} can be configured globally in XML configuration
* with the Spring MVC namespace or in Java-based configuration with @{@link EnableWebMvc}.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {
public RequestPartMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) {
super(messageConverters);
}
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestPart.class);
}
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
ServletRequest servletRequest = webRequest.getNativeRequest(ServletRequest.class);
MultipartHttpServletRequest multipartServletRequest =
WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
if (multipartServletRequest == null) {
throw new IllegalStateException(
"Current request is not of type " + MultipartRequest.class.getName());
}
String partName = getPartName(parameter);
HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(multipartServletRequest, partName);
Object arg = readWithMessageConverters(inputMessage, parameter, parameter.getParameterType());
if (isValidationApplicable(arg, parameter)) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, partName);
binder.validate();
Errors errors = binder.getBindingResult();
if (errors.hasErrors()) {
throw new RequestPartNotValidException(errors);
}
}
return arg;
}
private String getPartName(MethodParameter parameter) {
RequestPart annot = parameter.getParameterAnnotation(RequestPart.class);
String partName = annot.value();
if (partName.length() == 0) {
partName = parameter.getParameterName();
Assert.notNull(partName, "Request part name for argument type [" + parameter.getParameterType().getName()
+ "] not available, and parameter name information not found in class file either.");
}
return partName;
}
/**
* Whether to validate the given @{@link RequestPart} method argument. The default implementation checks
* if the parameter is also annotated with {@code @Valid}.
* @param argumentValue the validation candidate
* @param parameter the method argument declaring the validation candidate
* @return {@code true} if validation should be invoked, {@code false} otherwise.
*/
protected boolean isValidationApplicable(Object argumentValue, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) {
if ("Valid".equals(annot.annotationType().getSimpleName())) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2002-2011 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation.support;
import org.springframework.validation.Errors;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
/**
* Thrown by {@link RequestPartMethodArgumentResolver} when an @{@link RequestPart} argument also annotated with
* {@code @Valid} results in validation errors.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
@SuppressWarnings("serial")
public class RequestPartNotValidException extends RuntimeException {
private final Errors errors;
/**
* @param errors contains the results of validating an @{@link RequestBody} argument.
*/
public RequestPartNotValidException(Errors errors) {
this.errors = errors;
}
/**
* Returns an Errors instance with validation errors.
*/
public Errors getErrors() {
return errors;
}
@Override
public String getMessage() {
StringBuilder sb = new StringBuilder(
"Validation of the content of request part '" + errors.getObjectName() + "' failed: ");
sb.append(errors.getErrorCount()).append(" errors");
for (ObjectError error : errors.getAllErrors()) {
sb.append('\n').append(error);
}
return sb.toString();
}
}

View File

@ -33,14 +33,16 @@ import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
/** /**
* Resolves method arguments annotated with @{@link RequestBody} and handles return values from methods * Resolves method arguments annotated with @{@link RequestBody} and handles return values from methods
* annotated with {@link ResponseBody}. * annotated with {@link ResponseBody}.
* *
* <p>An @{@link RequestBody} method argument will be validated if annotated with {@code @Valid}. A * <p>An @{@link RequestBody} method argument will be validated if annotated with {@code @Valid}. In case of
* {@link Validator} instance can be configured globally in XML configuration with the Spring MVC namespace * validation failure, a {@link RequestBodyNotValidException} is thrown and can be handled automatically through
* or in Java-based configuration with @{@link EnableWebMvc}. * the {@link DefaultHandlerExceptionResolver}. A {@link Validator} can be configured globally in XML configuration
* with the Spring MVC namespace or in Java-based configuration with @{@link EnableWebMvc}.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
@ -65,9 +67,9 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
NativeWebRequest webRequest, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception { WebDataBinderFactory binderFactory) throws Exception {
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getParameterType()); Object arg = readWithMessageConverters(webRequest, parameter, parameter.getParameterType());
if (shouldValidate(parameter, arg)) { if (isValidationApplicable(arg, parameter)) {
String argName = Conventions.getVariableNameForParameter(parameter); String name = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, argName); WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
binder.validate(); binder.validate();
Errors errors = binder.getBindingResult(); Errors errors = binder.getBindingResult();
if (errors.hasErrors()) { if (errors.hasErrors()) {
@ -80,11 +82,11 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
/** /**
* Whether to validate the given @{@link RequestBody} method argument. The default implementation checks * Whether to validate the given @{@link RequestBody} method argument. The default implementation checks
* if the parameter is also annotated with {@code @Valid}. * if the parameter is also annotated with {@code @Valid}.
* @param parameter the method argument for which to check if validation is needed * @param argumentValue the validation candidate
* @param argumentValue the method argument value (instantiated with a message converter) * @param parameter the method argument declaring the validation candidate
* @return {@code true} if validation should be invoked, {@code false} otherwise. * @return {@code true} if validation should be invoked, {@code false} otherwise.
*/ */
protected boolean shouldValidate(MethodParameter parameter, Object argumentValue) { protected boolean isValidationApplicable(Object argumentValue, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations(); Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) { for (Annotation annot : annotations) {
if ("Valid".equals(annot.annotationType().getSimpleName())) { if ("Valid".equals(annot.annotationType().getSimpleName())) {

View File

@ -40,6 +40,7 @@ import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.support.RequestBodyNotValidException; import org.springframework.web.servlet.mvc.method.annotation.support.RequestBodyNotValidException;
import org.springframework.web.servlet.mvc.method.annotation.support.RequestPartNotValidException;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
/** /**
@ -129,6 +130,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
else if (ex instanceof RequestBodyNotValidException) { else if (ex instanceof RequestBodyNotValidException) {
return handleRequestBodyNotValidException((RequestBodyNotValidException) ex, request, response, handler); return handleRequestBodyNotValidException((RequestBodyNotValidException) ex, request, response, handler);
} }
else if (ex instanceof RequestPartNotValidException) {
return handleRequestPartNotValidException((RequestPartNotValidException) ex, request, response, handler);
}
} }
catch (Exception handlerException) { catch (Exception handlerException) {
logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException); logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException);
@ -339,8 +343,8 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
} }
/** /**
* Handle the case where the object created from the body of a request has failed validation. The default * Handle the case where the object created from the body of a request has failed validation.
* implementation sends an HTTP 400 error along with a message containing the errors. * The default implementation sends an HTTP 400 error along with a message containing the errors.
* @param request current HTTP request * @param request current HTTP request
* @param response current HTTP response * @param response current HTTP response
* @param handler the executed handler * @param handler the executed handler
@ -353,4 +357,19 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
return new ModelAndView(); return new ModelAndView();
} }
/**
* Handle the case where the object created from the part of a multipart request has failed validation.
* The default implementation sends an HTTP 400 error along with a message containing the errors.
* @param request current HTTP request
* @param response current HTTP response
* @param handler the executed handler
* @return an empty ModelAndView indicating the exception was handled
* @throws IOException potentially thrown from response.sendError()
*/
protected ModelAndView handleRequestPartNotValidException(RequestPartNotValidException ex,
HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
} }

View File

@ -52,6 +52,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.mock.web.MockMultipartHttpServletRequest;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
@ -65,6 +67,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.annotation.SessionAttributes;
@ -244,6 +247,18 @@ public class RequestMappingHandlerAdapterIntegrationTests {
assertEquals("headerValue", response.getHeader("header")); assertEquals("headerValue", response.getHeader("header"));
} }
@Test
public void handleRequestPart() throws Exception {
MockMultipartHttpServletRequest multipartRequest = new MockMultipartHttpServletRequest();
multipartRequest.addFile(new MockMultipartFile("requestPart", "", "text/plain", "content".getBytes("UTF-8")));
HandlerMethod handlerMethod = handlerMethod("handleRequestPart", String.class, Model.class);
ModelAndView mav = handlerAdapter.handle(multipartRequest, response, handlerMethod);
assertNotNull(mav);
assertEquals("content", mav.getModelMap().get("requestPart"));
}
private HandlerMethod handlerMethod(String methodName, Class<?>... paramTypes) throws Exception { private HandlerMethod handlerMethod(String methodName, Class<?>... paramTypes) throws Exception {
Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes); Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes);
return new InvocableHandlerMethod(handler, method); return new InvocableHandlerMethod(handler, method);
@ -317,6 +332,10 @@ public class RequestMappingHandlerAdapterIntegrationTests {
String responseBody = "Handled requestBody=[" + new String(httpEntity.getBody(), "UTF-8") + "]"; String responseBody = "Handled requestBody=[" + new String(httpEntity.getBody(), "UTF-8") + "]";
return new ResponseEntity<String>(responseBody, responseHeaders, HttpStatus.ACCEPTED); return new ResponseEntity<String>(responseBody, responseHeaders, HttpStatus.ACCEPTED);
} }
public void handleRequestPart(@RequestPart String requestPart, Model model) {
model.addAttribute("requestPart", requestPart);
}
} }
private static class StubValidator implements Validator { private static class StubValidator implements Validator {

View File

@ -0,0 +1,187 @@
/*
* Copyright 2002-2011 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation.support;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Collections;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.mock.web.MockMultipartHttpServletRequest;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.RequestPartServletServerHttpRequest;
/**
* Test fixture with {@link RequestPartMethodArgumentResolver} and mock {@link HttpMessageConverter}.
*
* @author Rossen Stoyanchev
*/
public class RequestPartMethodArgumentResolverTests {
private RequestPartMethodArgumentResolver resolver;
private HttpMessageConverter<SimpleBean> messageConverter;
private MultipartFile multipartFile;
private MethodParameter paramRequestPart;
private MethodParameter paramNamedRequestPart;
private MethodParameter paramValidRequestPart;
private MethodParameter paramInt;
private NativeWebRequest webRequest;
private MockMultipartHttpServletRequest servletRequest;
private MockHttpServletResponse servletResponse;
@SuppressWarnings("unchecked")
@Before
public void setUp() throws Exception {
Method handle = getClass().getMethod("handle", SimpleBean.class, SimpleBean.class, SimpleBean.class, Integer.TYPE);
paramRequestPart = new MethodParameter(handle, 0);
paramRequestPart.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer());
paramNamedRequestPart = new MethodParameter(handle, 1);
paramValidRequestPart = new MethodParameter(handle, 2);
paramInt = new MethodParameter(handle, 3);
messageConverter = createMock(HttpMessageConverter.class);
expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
replay(messageConverter);
resolver = new RequestPartMethodArgumentResolver(Collections.<HttpMessageConverter<?>>singletonList(messageConverter));
reset(messageConverter);
multipartFile = new MockMultipartFile("requestPart", "", "text/plain", (byte[]) null);
servletRequest = new MockMultipartHttpServletRequest();
servletRequest.addFile(multipartFile);
servletResponse = new MockHttpServletResponse();
webRequest = new ServletWebRequest(servletRequest, servletResponse);
}
@Test
public void supportsParameter() {
assertTrue("RequestPart parameter not supported", resolver.supportsParameter(paramRequestPart));
assertFalse("non-RequestPart parameter supported", resolver.supportsParameter(paramInt));
}
@Test
public void resolveRequestPart() throws Exception {
testResolveArgument(new SimpleBean("foo"), paramRequestPart);
}
@Test
public void resolveNamedRequestPart() throws Exception {
testResolveArgument(new SimpleBean("foo"), paramNamedRequestPart);
}
@Test
public void resolveRequestPartNotValid() throws Exception {
try {
testResolveArgument(new SimpleBean(null), paramValidRequestPart);
fail("Expected exception");
} catch (RequestPartNotValidException e) {
assertEquals("requestPart", e.getErrors().getObjectName());
assertEquals(1, e.getErrors().getErrorCount());
assertNotNull(e.getErrors().getFieldError("name"));
}
}
@Test
public void resolveRequestPartValid() throws Exception {
testResolveArgument(new SimpleBean("foo"), paramValidRequestPart);
}
private void testResolveArgument(SimpleBean expectedValue, MethodParameter parameter) throws IOException, Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
expect(messageConverter.canRead(SimpleBean.class, contentType)).andReturn(true);
expect(messageConverter.read(eq(SimpleBean.class), isA(RequestPartServletServerHttpRequest.class))).andReturn(expectedValue);
replay(messageConverter);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
Object actualValue = resolver.resolveArgument(parameter, mavContainer, webRequest, new ValidatingBinderFactory());
assertEquals("Invalid argument value", expectedValue, actualValue);
assertTrue("The ResolveView flag shouldn't change", mavContainer.isResolveView());
verify(messageConverter);
}
public void handle(@RequestPart SimpleBean requestPart,
@RequestPart("requestPart") SimpleBean namedRequestPart,
@Valid @RequestPart("requestPart") SimpleBean validRequestPart,
int i) {
}
private static class SimpleBean {
@NotNull
private final String name;
public SimpleBean(String name) {
this.name = name;
}
@SuppressWarnings("unused")
public String getName() {
return name;
}
}
private final class ValidatingBinderFactory implements WebDataBinderFactory {
public WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
WebDataBinder dataBinder = new WebDataBinder(target, objectName);
dataBinder.setValidator(validator);
return dataBinder;
}
}
}

View File

@ -29,6 +29,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.io.IOException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -104,7 +105,7 @@ public class RequestResponseBodyMethodProcessorTests {
returnTypeString = new MethodParameter(handle, -1); returnTypeString = new MethodParameter(handle, -1);
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", ValidBean.class), 0); paramValidBean = new MethodParameter(getClass().getMethod("handle4", SimpleBean.class), 0);
mavContainer = new ModelAndViewContainer(); mavContainer = new ModelAndViewContainer();
@ -142,43 +143,38 @@ public class RequestResponseBodyMethodProcessorTests {
verify(messageConverter); verify(messageConverter);
} }
@SuppressWarnings("unchecked")
@Test @Test
public void resolveArgumentNotValid() throws Exception { public void resolveArgumentNotValid() throws Exception {
MediaType contentType = MediaType.TEXT_PLAIN;
servletRequest.addHeader("Content-Type", contentType.toString());
HttpMessageConverter<ValidBean> beanConverter = createMock(HttpMessageConverter.class);
expect(beanConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(beanConverter.canRead(ValidBean.class, contentType)).andReturn(true);
expect(beanConverter.read(eq(ValidBean.class), isA(HttpInputMessage.class))).andReturn(new ValidBean(null));
replay(beanConverter);
processor = new RequestResponseBodyMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(beanConverter));
try { try {
processor.resolveArgument(paramValidBean, mavContainer, webRequest, new ValidatingBinderFactory()); testResolveArgumentWithValidation(new SimpleBean(null));
fail("Expected exception"); fail("Expected exception");
} catch (RequestBodyNotValidException e) { } catch (RequestBodyNotValidException e) {
assertEquals("validBean", e.getErrors().getObjectName()); assertEquals("simpleBean", e.getErrors().getObjectName());
assertEquals(1, e.getErrors().getErrorCount()); assertEquals(1, e.getErrors().getErrorCount());
assertNotNull(e.getErrors().getFieldError("name")); assertNotNull(e.getErrors().getFieldError("name"));
} }
} }
@SuppressWarnings("unchecked")
@Test @Test
public void resolveArgumentValid() throws Exception { public void resolveArgumentValid() throws Exception {
testResolveArgumentWithValidation(new SimpleBean("name"));
}
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());
HttpMessageConverter<ValidBean> beanConverter = createMock(HttpMessageConverter.class); @SuppressWarnings("unchecked")
HttpMessageConverter<SimpleBean> beanConverter = createMock(HttpMessageConverter.class);
expect(beanConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN)); expect(beanConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
expect(beanConverter.canRead(ValidBean.class, contentType)).andReturn(true); expect(beanConverter.canRead(SimpleBean.class, contentType)).andReturn(true);
expect(beanConverter.read(eq(ValidBean.class), isA(HttpInputMessage.class))).andReturn(new ValidBean("name")); expect(beanConverter.read(eq(SimpleBean.class), isA(HttpInputMessage.class))).andReturn(simpleBean);
replay(beanConverter); replay(beanConverter);
processor = new RequestResponseBodyMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(beanConverter)); processor = new RequestResponseBodyMethodProcessor(Collections.<HttpMessageConverter<?>>singletonList(beanConverter));
processor.resolveArgument(paramValidBean, mavContainer, webRequest, new ValidatingBinderFactory()); processor.resolveArgument(paramValidBean, mavContainer, webRequest, new ValidatingBinderFactory());
verify(beanConverter);
} }
@Test(expected = HttpMediaTypeNotSupportedException.class) @Test(expected = HttpMediaTypeNotSupportedException.class)
@ -293,7 +289,7 @@ public class RequestResponseBodyMethodProcessorTests {
return null; return null;
} }
public void handle4(@Valid @RequestBody ValidBean b) { public void handle4(@Valid @RequestBody SimpleBean b) {
} }
private final class ValidatingBinderFactory implements WebDataBinderFactory { private final class ValidatingBinderFactory implements WebDataBinderFactory {
@ -307,12 +303,12 @@ public class RequestResponseBodyMethodProcessorTests {
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static class ValidBean { private static class SimpleBean {
@NotNull @NotNull
private final String name; private final String name;
public ValidBean(String name) { public SimpleBean(String name) {
this.name = name; this.name = name;
} }

View File

@ -38,6 +38,7 @@ import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.support.RequestBodyNotValidException; import org.springframework.web.servlet.mvc.method.annotation.support.RequestBodyNotValidException;
import org.springframework.web.servlet.mvc.method.annotation.support.RequestPartNotValidException;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
/** @author Arjen Poutsma */ /** @author Arjen Poutsma */
@ -147,4 +148,16 @@ public class DefaultHandlerExceptionResolverTests {
assertTrue(response.getErrorMessage().contains("Field error in object 'testBean' on field 'name'")); assertTrue(response.getErrorMessage().contains("Field error in object 'testBean' on field 'name'"));
} }
@Test
public void handleRequestPartNotValid() {
BeanPropertyBindingResult errors = new BeanPropertyBindingResult(new TestBean(), "testBean");
errors.rejectValue("name", "invalid");
RequestPartNotValidException ex = new RequestPartNotValidException(errors);
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
assertNotNull("No ModelAndView returned", mav);
assertTrue("No Empty ModelAndView returned", mav.isEmpty());
assertEquals("Invalid status code", 400, response.getStatus());
assertTrue(response.getErrorMessage().startsWith("Validation of the content of request part"));
assertTrue(response.getErrorMessage().contains("Field error in object 'testBean' on field 'name'"));
}
} }

View File

@ -35,7 +35,7 @@ import java.lang.annotation.Target;
* @see RequestParam * @see RequestParam
* @see RequestHeader * @see RequestHeader
* @see org.springframework.web.bind.annotation.RequestMapping * @see org.springframework.web.bind.annotation.RequestMapping
* @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodAdapter
* @see org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter * @see org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter
*/ */
@Target(ElementType.PARAMETER) @Target(ElementType.PARAMETER)

View File

@ -12,7 +12,7 @@ import java.lang.annotation.Target;
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @see RequestMapping * @see RequestMapping
* @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodAdapter
* @since 3.0 * @since 3.0
*/ */
@Target(ElementType.PARAMETER) @Target(ElementType.PARAMETER)

View File

@ -29,7 +29,7 @@ import java.lang.annotation.Target;
* @author Arjen Poutsma * @author Arjen Poutsma
* @see RequestHeader * @see RequestHeader
* @see ResponseBody * @see ResponseBody
* @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodAdapter
* @since 3.0 * @since 3.0
*/ */
@Target(ElementType.PARAMETER) @Target(ElementType.PARAMETER)

View File

@ -31,7 +31,7 @@ import java.lang.annotation.Target;
* @see RequestMapping * @see RequestMapping
* @see RequestParam * @see RequestParam
* @see CookieValue * @see CookieValue
* @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodAdapter
* @see org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter * @see org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter
*/ */
@Target(ElementType.PARAMETER) @Target(ElementType.PARAMETER)

View File

@ -95,6 +95,11 @@ import java.lang.annotation.Target;
* converted to the declared method argument type using * converted to the declared method argument type using
* {@linkplain org.springframework.http.converter.HttpMessageConverter message * {@linkplain org.springframework.http.converter.HttpMessageConverter message
* converters}. Such parameters may optionally be annotated with {@code @Valid}. * converters}. Such parameters may optionally be annotated with {@code @Valid}.
* <li>{@link RequestPart @RequestPart} annotated parameters for access to the content
* of a part of "multipart/form-data" request. The request part stream will be
* converted to the declared method argument type using
* {@linkplain org.springframework.http.converter.HttpMessageConverter message
* converters}. Such parameters may optionally be annotated with {@code @Valid}.
* <li>{@link org.springframework.http.HttpEntity HttpEntity&lt;?&gt;} parameters * <li>{@link org.springframework.http.HttpEntity HttpEntity&lt;?&gt;} parameters
* for access to the Servlet request HTTP headers and contents. The request stream will be * for access to the Servlet request HTTP headers and contents. The request stream will be
* converted to the entity body using * converted to the entity body using

View File

@ -33,7 +33,7 @@ import java.lang.annotation.Target;
* @see RequestMapping * @see RequestMapping
* @see RequestHeader * @see RequestHeader
* @see CookieValue * @see CookieValue
* @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodAdapter
* @see org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter * @see org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter
*/ */
@Target(ElementType.PARAMETER) @Target(ElementType.PARAMETER)

View File

@ -0,0 +1,44 @@
/*
* Copyright 2002-2009 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation that indicates a method parameter should be bound to the content of a part of a "multipart/form-data" request.
* Supported for annotated handler methods in Servlet environments.
*
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
* @since 3.1
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestPart {
/**
* The name of the part in the "multipart/form-data" request to bind to.
*/
String value() default "";
}

View File

@ -60,7 +60,7 @@ public class ExpressionValueMethodArgumentResolver extends AbstractNamedValueMet
@Override @Override
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest)
throws Exception { throws Exception {
// There is no name to be resolved // No name to resolve
return null; return null;
} }

View File

@ -102,7 +102,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
if (binder.getTarget() != null) { if (binder.getTarget() != null) {
bindRequestParameters(binder, request); bindRequestParameters(binder, request);
if (isValidationApplicable(binder, parameter)) { if (isValidationApplicable(binder.getTarget(), parameter)) {
binder.validate(); binder.validate();
} }
@ -148,12 +148,12 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
} }
/** /**
* Whether to validate the model attribute inside the given data binder instance. * Whether to validate the given model attribute argument value.
* @param binder the data binder containing the validation candidate * @param argumentValue the validation candidate
* @param parameter the method argument declaring the validation candidate * @param parameter the method argument declaring the validation candidate
* @return {@code true} if validation should be applied, {@code false} otherwise. * @return {@code true} if validation should be applied, {@code false} otherwise.
*/ */
protected boolean isValidationApplicable(WebDataBinder binder, MethodParameter parameter) { protected boolean isValidationApplicable(Object argumentValue, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations(); Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) { for (Annotation annot : annotations) {
if ("Valid".equals(annot.annotationType().getSimpleName())) { if ("Valid".equals(annot.annotationType().getSimpleName())) {

View File

@ -0,0 +1,98 @@
/*
* Copyright 2002-2011 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.multipart;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.util.Assert;
/**
* {@link ServerHttpRequest} implementation that is based on a part of a {@link MultipartHttpServletRequest}.
* The part is accessed as {@link MultipartFile} and adapted to the ServerHttpRequest contract.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
public class RequestPartServletServerHttpRequest implements ServerHttpRequest {
private final MultipartHttpServletRequest request;
private final MultipartFile multipartFile;
private HttpHeaders headers;
/**
* Creates a new {@link RequestPartServletServerHttpRequest} instance.
*
* @param request the multipart request.
* @param name the name of the part to adapt to the {@link ServerHttpRequest} contract.
*/
public RequestPartServletServerHttpRequest(MultipartHttpServletRequest request, String name) {
this.request = request;
this.multipartFile = request.getFile(name);
Assert.notNull(multipartFile, "Request part named '" + name + "' not found. " +
"Available request part names: " + request.getMultiFileMap().keySet());
}
public HttpMethod getMethod() {
return HttpMethod.valueOf(this.request.getMethod());
}
public URI getURI() {
try {
return new URI(this.request.getScheme(), null, this.request.getServerName(),
this.request.getServerPort(), this.request.getRequestURI(),
this.request.getQueryString(), null);
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Could not get HttpServletRequest URI: " + ex.getMessage(), ex);
}
}
/**
* Returns the headers associated with the part of the multi-part request associated with this instance.
* If the underlying implementation supports access to headers, then all headers are returned.
* Otherwise, the returned headers will have a 'Content-Type' header in the very least.
*/
public HttpHeaders getHeaders() {
if (this.headers == null) {
this.headers = new HttpHeaders();
Iterator<String> iterator = this.multipartFile.getHeaderNames();
while (iterator.hasNext()) {
String name = iterator.next();
String[] values = this.multipartFile.getHeaders(name);
for (String value : values) {
this.headers.add(name, value);
}
}
}
return this.headers;
}
public InputStream getBody() throws IOException {
return this.multipartFile.getInputStream();
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright 2002-2011 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.multipart;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.net.URI;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.mock.web.MockMultipartHttpServletRequest;
import org.springframework.util.FileCopyUtils;
/**
* Test fixture for {@link RequestPartServletServerHttpRequest} unit tests.
*
* @author Rossen Stoyanchev
*/
public class RequestPartServletServerHttpRequestTests {
private RequestPartServletServerHttpRequest request;
private MockMultipartHttpServletRequest mockRequest;
private MockMultipartFile mockFile;
@Before
public void create() throws Exception {
mockFile = new MockMultipartFile("part", "", "application/json" ,"Part Content".getBytes("UTF-8"));
mockRequest = new MockMultipartHttpServletRequest();
mockRequest.addFile(mockFile);
request = new RequestPartServletServerHttpRequest(mockRequest, "part");
}
@Test
public void getMethod() throws Exception {
mockRequest.setMethod("POST");
assertEquals("Invalid method", HttpMethod.POST, request.getMethod());
}
@Test
public void getURI() throws Exception {
URI uri = new URI("http://example.com/path?query");
mockRequest.setServerName(uri.getHost());
mockRequest.setServerPort(uri.getPort());
mockRequest.setRequestURI(uri.getPath());
mockRequest.setQueryString(uri.getQuery());
assertEquals("Invalid uri", uri, request.getURI());
}
@Test
public void getContentType() throws Exception {
HttpHeaders headers = request.getHeaders();
assertNotNull("No HttpHeaders returned", headers);
MediaType expected = MediaType.parseMediaType(mockFile.getContentType());
MediaType actual = headers.getContentType();
assertEquals("Invalid content type returned", expected, actual);
}
@Test
public void getBody() throws Exception {
byte[] result = FileCopyUtils.copyToByteArray(request.getBody());
assertArrayEquals("Invalid content returned", mockFile.getBytes(), result);
}
}

View File

@ -1109,6 +1109,14 @@ public class RelativePathUriTemplateController {
linkend="mvc-ann-requestbody" />.</para> linkend="mvc-ann-requestbody" />.</para>
</listitem> </listitem>
<listitem>
<para><interfacename>@RequestPart</interfacename> annotated parameters
for access to the content of a "multipart/form-data" request part.
Parameter values are converted to the declared method argument type using
<interfacename>HttpMessageConverter</interfacename>s. See <xref
linkend="mvc-ann-requestpart" />.</para>
</listitem>
<listitem> <listitem>
<para><classname>HttpEntity&lt;?&gt;</classname> parameters <para><classname>HttpEntity&lt;?&gt;</classname> parameters
for access to the Servlet request HTTP headers and contents. The request stream will be for access to the Servlet request HTTP headers and contents. The request stream will be
@ -1398,7 +1406,7 @@ public void handle(@RequestBody String body, Writer writer) throws IOException {
validator is configured automatically assuming a JSR-303 implementation is available validator is configured automatically assuming a JSR-303 implementation is available
on the classpath. If validation fails a <classname>RequestBodyNotValidException</classname> on the classpath. If validation fails a <classname>RequestBodyNotValidException</classname>
is raised. The exception is handled by the <classname>DefaultHandlerExceptionResolver</classname> is raised. The exception is handled by the <classname>DefaultHandlerExceptionResolver</classname>
and results in a <literal>500</literal> error send back to the client along with and results in a <literal>400</literal> error sent back to the client along with
a message containing the validation errors.</para> a message containing the validation errors.</para>
<note> <note>
@ -1407,6 +1415,67 @@ public void handle(@RequestBody String body, Writer writer) throws IOException {
</note> </note>
</section> </section>
<section id="mvc-ann-requestpart">
<title>Mapping the content of a part of a "multipart/form-data" request with the
<interfacename>@RequestPart</interfacename> annotation</title>
<para>A "multipart/form-data" request contains a series of parts each with its own
headers and content. It is commonly used for handling file uploads on a form --
see <xref linkend="mvc-multipart"/> -- but can also be used to send or receive
a request with multiple types of content.</para>
<para>The <interfacename>@RequestPart</interfacename> annotation works very similarly to the
<interfacename>@RequestBody</interfacename> annotation except instead of looking in the
body of the HTTP request, it binds the method parameter to the content of one of the
parts of a "multipart/form-data" request. Here is an exampe:</para>
<programlisting language="java">
@RequestMapping(value="/configurations", method = RequestMethod.POST)
public String onSubmit(<emphasis role="bold">@RequestPart("meta-data") MetaData metadata</emphasis>) {
<lineannotation>// ...</lineannotation>
}
</programlisting>
<para>The actual request may look like this:</para>
<programlisting language="xml">
POST /configurations
Content-Type: multipart/mixed
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit
{
"name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...
</programlisting>
<para>In the above example, the <literal>metadata</literal> argument is bound to the content
of the first part of the request called <literal>"meta-data"</literal> containing JSON content.
In this case we specified the name of the request part in the
<interfacename>@RequestPart</interfacename> annotation but we might have been able to leave it
out if the name of the method argument matched the request part name.</para>
<para>Just like with <interfacename>@RequestBody</interfacename> you convert the content of
the request part to the method argument type by using an
<classname>HttpMessageConverter</classname>. Also you can add <literal>@Valid</literal>
to the method argument to have the resulting object automatically validated.
If validation fails a <classname>RequestPartNotValidException</classname> is raised.
The exception is handled by the <classname>DefaultHandlerExceptionResolver</classname> and
results in a <literal>400</literal> error sent back to the client along with a message
containing the validation errors.</para>
</section>
<section id="mvc-ann-responsebody"> <section id="mvc-ann-responsebody">
<title>Mapping the response body with the <interfacename>@ResponseBody</interfacename> <title>Mapping the response body with the <interfacename>@ResponseBody</interfacename>
annotation</title> annotation</title>