diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index b662cc8f6ec..cc6e2db7ab5 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -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.ModelAndViewMethodReturnValueHandler; 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.ServletCookieValueMethodArgumentResolver; 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 ServletModelAttributeMethodProcessor(false)); argumentResolvers.addResolver(new RequestResponseBodyMethodProcessor(messageConverters)); + argumentResolvers.addResolver(new RequestPartMethodArgumentResolver(messageConverters)); argumentResolvers.addResolver(new RequestHeaderMethodArgumentResolver(beanFactory)); argumentResolvers.addResolver(new RequestHeaderMapMethodArgumentResolver()); argumentResolvers.addResolver(new ServletCookieValueMethodArgumentResolver(beanFactory)); diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/AbstractMessageConverterMethodArgumentResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/AbstractMessageConverterMethodArgumentResolver.java new file mode 100644 index 00000000000..c1927d83b90 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/AbstractMessageConverterMethodArgumentResolver.java @@ -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> messageConverters; + + protected final List allSupportedMediaTypes; + + public AbstractMessageConverterMethodArgumentResolver(List> 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 getAllSupportedMediaTypes(List> messageConverters) { + Set allSupportedMediaTypes = new LinkedHashSet(); + for (HttpMessageConverter messageConverter : messageConverters) { + allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); + } + List result = new ArrayList(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 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 Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam, Class 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 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 Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter methodParam, Class 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) 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); + } + +} \ No newline at end of file diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/AbstractMessageConverterMethodProcessor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/AbstractMessageConverterMethodProcessor.java index 2b7e6506630..b964d700280 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/AbstractMessageConverterMethodProcessor.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/AbstractMessageConverterMethodProcessor.java @@ -27,8 +27,6 @@ import java.util.Set; import javax.servlet.http.HttpServletRequest; 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.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; @@ -36,90 +34,27 @@ import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.web.HttpMediaTypeNotAcceptableException; -import org.springframework.web.HttpMediaTypeNotSupportedException; 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.servlet.HandlerMapping; /** - * A base class for resolving method argument values by reading from the body of a request with {@link - * HttpMessageConverter}s and for handling method return values by writing to the response with {@link - * HttpMessageConverter}s. + * Extends {@link AbstractMessageConverterMethodArgumentResolver} with the ability to handle method return + * values by writing to the response with {@link HttpMessageConverter}s. * * @author Arjen Poutsma * @author Rossen Stoyanchev * @since 3.1 */ -public abstract class AbstractMessageConverterMethodProcessor - implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { +public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver + implements HandlerMethodReturnValueHandler { private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application"); - protected final Log logger = LogFactory.getLog(getClass()); - - private final List> messageConverters; - - private final List allSupportedMediaTypes; - protected AbstractMessageConverterMethodProcessor(List> 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 getAllSupportedMediaTypes(List> messageConverters) { - Set allSupportedMediaTypes = new LinkedHashSet(); - for (HttpMessageConverter messageConverter : messageConverters) { - allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); - } - List result = new ArrayList(allSupportedMediaTypes); - MediaType.sortBySpecificity(result); - return Collections.unmodifiableList(result); - } - - @SuppressWarnings("unchecked") - protected Object readWithMessageConverters(NativeWebRequest webRequest, - MethodParameter methodParam, - Class 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) 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); + super(messageConverters); } /** diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartMethodArgumentResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartMethodArgumentResolver.java new file mode 100644 index 00000000000..259c1240c8e --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartMethodArgumentResolver.java @@ -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. + * + *

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> 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; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartNotValidException.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartNotValidException.java new file mode 100644 index 00000000000..2d2f782e85a --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartNotValidException.java @@ -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(); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java index 4fcb9c73d55..092f2c4b92c 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java @@ -33,14 +33,16 @@ 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.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 * annotated with {@link ResponseBody}. * - *

An @{@link RequestBody} method argument will be validated if annotated with {@code @Valid}. A - * {@link Validator} instance can be configured globally in XML configuration with the Spring MVC namespace - * or in Java-based configuration with @{@link EnableWebMvc}. + *

An @{@link RequestBody} method argument will be validated if annotated with {@code @Valid}. In case of + * validation failure, a {@link RequestBodyNotValidException} 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 Arjen Poutsma * @author Rossen Stoyanchev @@ -65,9 +67,9 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { Object arg = readWithMessageConverters(webRequest, parameter, parameter.getParameterType()); - if (shouldValidate(parameter, arg)) { - String argName = Conventions.getVariableNameForParameter(parameter); - WebDataBinder binder = binderFactory.createBinder(webRequest, arg, argName); + if (isValidationApplicable(arg, parameter)) { + String name = Conventions.getVariableNameForParameter(parameter); + WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); binder.validate(); Errors errors = binder.getBindingResult(); 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 * 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 method argument value (instantiated with a message converter) + * @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 shouldValidate(MethodParameter parameter, Object argumentValue) { + protected boolean isValidationApplicable(Object argumentValue, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation annot : annotations) { if ("Valid".equals(annot.annotationType().getSimpleName())) { diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index f9566f1de8c..00b011bb2b5 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -40,6 +40,7 @@ import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.servlet.ModelAndView; 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.RequestPartNotValidException; import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; /** @@ -129,6 +130,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes else if (ex instanceof RequestBodyNotValidException) { return handleRequestBodyNotValidException((RequestBodyNotValidException) ex, request, response, handler); } + else if (ex instanceof RequestPartNotValidException) { + return handleRequestPartNotValidException((RequestPartNotValidException) ex, request, response, handler); + } } catch (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 - * implementation sends an HTTP 400 error along with a message containing the errors. + * Handle the case where the object created from the body of a 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 @@ -353,4 +357,19 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes 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(); + } + } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java index a22771de536..23f87e31034 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java @@ -52,6 +52,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockHttpServletRequest; 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.ModelMap; 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.RequestHeader; 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.ResponseStatus; import org.springframework.web.bind.annotation.SessionAttributes; @@ -244,6 +247,18 @@ public class RequestMappingHandlerAdapterIntegrationTests { 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 { Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes); return new InvocableHandlerMethod(handler, method); @@ -317,6 +332,10 @@ public class RequestMappingHandlerAdapterIntegrationTests { String responseBody = "Handled requestBody=[" + new String(httpEntity.getBody(), "UTF-8") + "]"; return new ResponseEntity(responseBody, responseHeaders, HttpStatus.ACCEPTED); } + + public void handleRequestPart(@RequestPart String requestPart, Model model) { + model.addAttribute("requestPart", requestPart); + } } private static class StubValidator implements Validator { diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartMethodArgumentResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartMethodArgumentResolverTests.java new file mode 100644 index 00000000000..6fa132a461e --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestPartMethodArgumentResolverTests.java @@ -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 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.>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; + } + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java index 9e0d66ab5ff..e96f19e6b43 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessorTests.java @@ -29,6 +29,7 @@ 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.ArrayList; import java.util.Arrays; @@ -104,7 +105,7 @@ public class RequestResponseBodyMethodProcessorTests { returnTypeString = new MethodParameter(handle, -1); returnTypeInt = new MethodParameter(getClass().getMethod("handle2"), -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(); @@ -142,43 +143,38 @@ public class RequestResponseBodyMethodProcessorTests { verify(messageConverter); } - @SuppressWarnings("unchecked") @Test public void resolveArgumentNotValid() throws Exception { - MediaType contentType = MediaType.TEXT_PLAIN; - servletRequest.addHeader("Content-Type", contentType.toString()); - - HttpMessageConverter 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.>singletonList(beanConverter)); try { - processor.resolveArgument(paramValidBean, mavContainer, webRequest, new ValidatingBinderFactory()); + testResolveArgumentWithValidation(new SimpleBean(null)); fail("Expected exception"); } catch (RequestBodyNotValidException e) { - assertEquals("validBean", e.getErrors().getObjectName()); + assertEquals("simpleBean", e.getErrors().getObjectName()); assertEquals(1, e.getErrors().getErrorCount()); assertNotNull(e.getErrors().getFieldError("name")); } } - - @SuppressWarnings("unchecked") + @Test public void resolveArgumentValid() throws Exception { + testResolveArgumentWithValidation(new SimpleBean("name")); + } + + private void testResolveArgumentWithValidation(SimpleBean simpleBean) throws IOException, Exception { MediaType contentType = MediaType.TEXT_PLAIN; servletRequest.addHeader("Content-Type", contentType.toString()); - HttpMessageConverter beanConverter = createMock(HttpMessageConverter.class); + @SuppressWarnings("unchecked") + HttpMessageConverter 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("name")); + expect(beanConverter.canRead(SimpleBean.class, contentType)).andReturn(true); + expect(beanConverter.read(eq(SimpleBean.class), isA(HttpInputMessage.class))).andReturn(simpleBean); replay(beanConverter); processor = new RequestResponseBodyMethodProcessor(Collections.>singletonList(beanConverter)); processor.resolveArgument(paramValidBean, mavContainer, webRequest, new ValidatingBinderFactory()); + + verify(beanConverter); } @Test(expected = HttpMediaTypeNotSupportedException.class) @@ -293,7 +289,7 @@ public class RequestResponseBodyMethodProcessorTests { return null; } - public void handle4(@Valid @RequestBody ValidBean b) { + public void handle4(@Valid @RequestBody SimpleBean b) { } private final class ValidatingBinderFactory implements WebDataBinderFactory { @@ -307,12 +303,12 @@ public class RequestResponseBodyMethodProcessorTests { } @SuppressWarnings("unused") - private static class ValidBean { + private static class SimpleBean { @NotNull private final String name; - public ValidBean(String name) { + public SimpleBean(String name) { this.name = name; } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java index 53aa6c22ed3..8c6c3f27383 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java @@ -38,6 +38,7 @@ import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.ServletRequestBindingException; 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.RequestPartNotValidException; import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; /** @author Arjen Poutsma */ @@ -147,4 +148,16 @@ public class DefaultHandlerExceptionResolverTests { 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'")); + } } diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java index 4a74acb4585..aab76733f16 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/CookieValue.java @@ -35,7 +35,7 @@ import java.lang.annotation.Target; * @see RequestParam * @see RequestHeader * @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 */ @Target(ElementType.PARAMETER) diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/PathVariable.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/PathVariable.java index 9fff5eff2e2..aa77c24e718 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/PathVariable.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/PathVariable.java @@ -12,7 +12,7 @@ import java.lang.annotation.Target; * * @author Arjen Poutsma * @see RequestMapping - * @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter + * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodAdapter * @since 3.0 */ @Target(ElementType.PARAMETER) diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java index ebf36beab64..1c2610d0c6a 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestBody.java @@ -29,7 +29,7 @@ import java.lang.annotation.Target; * @author Arjen Poutsma * @see RequestHeader * @see ResponseBody - * @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter + * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodAdapter * @since 3.0 */ @Target(ElementType.PARAMETER) diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java index 5e985d18e9e..ef962da7aa0 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java @@ -31,7 +31,7 @@ import java.lang.annotation.Target; * @see RequestMapping * @see RequestParam * @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 */ @Target(ElementType.PARAMETER) diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index b8b1c87398c..a7fbf13131e 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -95,6 +95,11 @@ import java.lang.annotation.Target; * 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}. + *

  • {@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}. *
  • {@link org.springframework.http.HttpEntity HttpEntity<?>} parameters * for access to the Servlet request HTTP headers and contents. The request stream will be * converted to the entity body using diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java index 0ed714f989b..591f4ad5f8b 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java @@ -33,7 +33,7 @@ import java.lang.annotation.Target; * @see RequestMapping * @see RequestHeader * @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 */ @Target(ElementType.PARAMETER) diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestPart.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestPart.java new file mode 100644 index 00000000000..4a7785b79ac --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestPart.java @@ -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 ""; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolver.java index 0a826dc3b36..9e128a0b46d 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolver.java +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ExpressionValueMethodArgumentResolver.java @@ -60,7 +60,7 @@ public class ExpressionValueMethodArgumentResolver extends AbstractNamedValueMet @Override protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception { - // There is no name to be resolved + // No name to resolve return null; } diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java index 56b5a36441a..90d68fc04df 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/ModelAttributeMethodProcessor.java @@ -102,7 +102,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol if (binder.getTarget() != null) { bindRequestParameters(binder, request); - if (isValidationApplicable(binder, parameter)) { + if (isValidationApplicable(binder.getTarget(), parameter)) { binder.validate(); } @@ -148,12 +148,12 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol } /** - * Whether to validate the model attribute inside the given data binder instance. - * @param binder the data binder containing the validation candidate + * Whether to validate the given model attribute argument value. + * @param argumentValue the validation candidate * @param parameter the method argument declaring the validation candidate * @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(); for (Annotation annot : annotations) { if ("Valid".equals(annot.annotationType().getSimpleName())) { diff --git a/org.springframework.web/src/main/java/org/springframework/web/multipart/RequestPartServletServerHttpRequest.java b/org.springframework.web/src/main/java/org/springframework/web/multipart/RequestPartServletServerHttpRequest.java new file mode 100644 index 00000000000..5bad0c5e9d9 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/multipart/RequestPartServletServerHttpRequest.java @@ -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 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(); + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/multipart/RequestPartServletServerHttpRequestTests.java b/org.springframework.web/src/test/java/org/springframework/web/multipart/RequestPartServletServerHttpRequestTests.java new file mode 100644 index 00000000000..4835310cd29 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/multipart/RequestPartServletServerHttpRequestTests.java @@ -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); + } + +} diff --git a/spring-framework-reference/src/mvc.xml b/spring-framework-reference/src/mvc.xml index 001ff47a3dd..136d36d437f 100644 --- a/spring-framework-reference/src/mvc.xml +++ b/spring-framework-reference/src/mvc.xml @@ -1109,6 +1109,14 @@ public class RelativePathUriTemplateController { linkend="mvc-ann-requestbody" />. + + @RequestPart 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 + HttpMessageConverters. See . + + HttpEntity<?> parameters 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 on the classpath. If validation fails a RequestBodyNotValidException is raised. The exception is handled by the DefaultHandlerExceptionResolver - and results in a 500 error send back to the client along with + and results in a 400 error sent back to the client along with a message containing the validation errors. @@ -1407,6 +1415,67 @@ public void handle(@RequestBody String body, Writer writer) throws IOException { +
    + Mapping the content of a part of a "multipart/form-data" request with the + <interfacename>@RequestPart</interfacename> annotation + + 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 -- but can also be used to send or receive + a request with multiple types of content. + + The @RequestPart annotation works very similarly to the + @RequestBody 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: + + +@RequestMapping(value="/configurations", method = RequestMethod.POST) +public String onSubmit(@RequestPart("meta-data") MetaData metadata) { + + // ... + +} + + + The actual request may look like this: + + +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 ... + + + In the above example, the metadata argument is bound to the content + of the first part of the request called "meta-data" containing JSON content. + In this case we specified the name of the request part in the + @RequestPart annotation but we might have been able to leave it + out if the name of the method argument matched the request part name. + + Just like with @RequestBody you convert the content of + the request part to the method argument type by using an + HttpMessageConverter. Also you can add @Valid + to the method argument to have the resulting object automatically validated. + If validation fails a RequestPartNotValidException is raised. + The exception is handled by the DefaultHandlerExceptionResolver and + results in a 400 error sent back to the client along with a message + containing the validation errors. + +
    +
    Mapping the response body with the <interfacename>@ResponseBody</interfacename> annotation