diff --git a/org.springframework.web.servlet/.classpath b/org.springframework.web.servlet/.classpath index 183b2aeee5..227369a8fb 100644 --- a/org.springframework.web.servlet/.classpath +++ b/org.springframework.web.servlet/.classpath @@ -5,23 +5,13 @@ - - - - - - - - - - - + @@ -50,6 +40,16 @@ + + + + + + + + + + diff --git a/org.springframework.web.servlet/ivy.xml b/org.springframework.web.servlet/ivy.xml index 8862a939bb..a77ff0c3fb 100644 --- a/org.springframework.web.servlet/ivy.xml +++ b/org.springframework.web.servlet/ivy.xml @@ -40,7 +40,7 @@ - + @@ -53,13 +53,21 @@ - + conf="optional, tiles->compile"> + + + + + + conf="optional, tiles->compile"> + + + conf="optional, tiles->compile"> + + javax.servlet servlet-api - 2.5 + 3.0 provided diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java index 0a98eb2824..70f8b5b829 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java @@ -17,6 +17,7 @@ package org.springframework.web.servlet.mvc.condition; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -32,13 +33,14 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression; /** - * A logical disjunction (' || ') request condition to match requests against producible media type expressions. + * A logical disjunction (' || ') request condition to match requests against producible + * media type expressions. * - *

For details on the syntax of the expressions see {@link RequestMapping#consumes()}. If the condition is - * created with 0 producible media type expressions, it matches to every request. + *

For details on the syntax of the expressions see {@link RequestMapping#consumes()}. + * If the condition is created without media type expressions, it matches to every request. * - *

This request condition is also capable of parsing header expressions specifically selecting 'Accept' header - * expressions and converting them to prodicuble media type expressions. + *

This request condition is also capable of parsing header expressions by selecting + * 'Accept' header expressions and converting them to prodicuble media type expressions. * * @author Arjen Poutsma * @author Rossen Stoyanchev diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/RequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/RequestCondition.java index 24c16bc7ce..caea6862d9 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/RequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/RequestCondition.java @@ -18,11 +18,14 @@ package org.springframework.web.servlet.mvc.condition; import javax.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.RequestMapping; + /** * The contract for request conditions. * - *

Request conditions can be combined (e.g. type + method-level conditions), matched to a request, - * or compared to each other to determine if one matches the request better. + *

Request conditions can be combined via {@link #combine(Object)}, matched to a request via + * {@link #getMatchingCondition(HttpServletRequest)}, and compared to each other via + * {@link #compareTo(Object, HttpServletRequest)} to determine which matches a request more closely. * * @param The type of objects that this RequestCondition can be compared to and combined with. * @@ -33,9 +36,10 @@ import javax.servlet.http.HttpServletRequest; public interface RequestCondition { /** - * Defines the rules for combining "this" condition (i.e. the current instance) with another condition. - *

Example: combine type- and method-level request mapping conditions. + * Defines the rules for combining this condition (i.e. the current instance) with another condition. + * For example combining type- and method-level {@link RequestMapping} conditions. * + * @param other the condition to combine with. * @returns a request condition instance that is the result of combining the two condition instances. */ T combine(T other); @@ -50,9 +54,9 @@ public interface RequestCondition { T getMatchingCondition(HttpServletRequest request); /** - * Compares "this" condition (i.e. the current instance) with another condition in the context of a request. - *

Note: it is assumed both instances have been obtained via {@link #getMatchingCondition(HttpServletRequest)} - * to ensure they have content relevant to current request only. + * Compares this condition to another condition in the context of a specific request. This method assumes + * both instances have been obtained via {@link #getMatchingCondition(HttpServletRequest)} to ensure they + * have content relevant to current request only. */ int compareTo(T other, HttpServletRequest request); diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index c0bbd8ca4f..28a1ade5e6 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -62,11 +62,10 @@ import org.springframework.web.servlet.mvc.method.annotation.support.ServletWebA import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodReturnValueHandler; /** - * An {@link AbstractHandlerMethodExceptionResolver} that looks for an {@link ExceptionHandler}-annotated method - * that can handle a thrown exception. If a match is found the exception-handling method is invoked to finish - * processing the request. + * An {@link AbstractHandlerMethodExceptionResolver} that supports using {@link ExceptionHandler}-annotated methods + * to resolve exceptions. * - *

{@link ExceptionMethodMapping} is a key contributing class storing method-to-exception type mappings extracted + *

{@link ExceptionMethodMapping} is a key contributing class that stores method-to-exception mappings extracted * from {@link ExceptionHandler} annotations or from the list of method arguments on the exception-handling method. * {@link ExceptionMethodMapping} assists with actually locating a method for a thrown exception. * 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 cc6e2db7ab..d8e839ffc7 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 @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -52,7 +53,6 @@ import org.springframework.web.bind.support.DefaultSessionAttributeStore; import org.springframework.web.bind.support.SessionAttributeStore; import org.springframework.web.bind.support.SessionStatus; import org.springframework.web.bind.support.SimpleSessionStatus; -import org.springframework.web.bind.support.WebArgumentResolver; import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.ServletWebRequest; @@ -90,7 +90,6 @@ import org.springframework.web.servlet.mvc.method.annotation.support.ServletCook import org.springframework.web.servlet.mvc.method.annotation.support.ServletModelAttributeMethodProcessor; import org.springframework.web.servlet.mvc.method.annotation.support.ServletRequestMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.support.ServletResponseMethodArgumentResolver; -import org.springframework.web.servlet.mvc.method.annotation.support.ServletWebArgumentResolverAdapter; import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodReturnValueHandler; import org.springframework.web.util.WebUtils; @@ -103,29 +102,30 @@ import org.springframework.web.util.WebUtils; * *

{@link InvocableHandlerMethod} is the key contributor that helps with the invocation of handler * methods of all types resolving their arguments through registered {@link HandlerMethodArgumentResolver}s. - * {@link ServletInvocableHandlerMethod} on the other hand adds handling of the return value for {@link RequestMapping} - * methods through registered {@link HandlerMethodReturnValueHandler}s resulting in a {@link ModelAndView}. + * {@link ServletInvocableHandlerMethod} on the other hand adds handling of the return value for + * {@link RequestMapping} methods through registered {@link HandlerMethodReturnValueHandler}s + * resulting in a {@link ModelAndView}. * *

{@link ModelFactory} is another contributor that assists with the invocation of all {@link ModelAttribute} * methods to populate a model while {@link ServletRequestDataBinderFactory} assists with the invocation of * {@link InitBinder} methods for initializing data binder instances when needed. * - *

This class is the central point that assembles all of mentioned contributors and invokes the actual + *

This class is the central point that assembles all mentioned contributors and invokes the actual * {@link RequestMapping} handler method through a {@link ServletInvocableHandlerMethod}. * * @author Rossen Stoyanchev * @since 3.1 - * @see InvocableHandlerMethod - * @see ServletInvocableHandlerMethod * @see HandlerMethodArgumentResolver * @see HandlerMethodReturnValueHandler + * @see #setCustomArgumentResolvers(List) + * @see #setCustomReturnValueHandlers(List) */ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { - private List customArgumentResolvers; + private List customArgumentResolvers; - private List customReturnValueHandlers; + private List customReturnValueHandlers; private List modelAndViewResolvers; @@ -146,16 +146,16 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i private final Map, SessionAttributesHandler> sessionAttributesHandlerCache = new ConcurrentHashMap, SessionAttributesHandler>(); - private final Map, Set> modelAttributeMethodCache = new ConcurrentHashMap, Set>(); - - private final Map, Set> initBinderMethodCache = new ConcurrentHashMap, Set>(); - - private HandlerMethodReturnValueHandlerComposite returnValueHandlers; - private HandlerMethodArgumentResolverComposite argumentResolvers; private HandlerMethodArgumentResolverComposite initBinderArgumentResolvers; + private HandlerMethodReturnValueHandlerComposite returnValueHandlers; + + private final Map, Set> initBinderMethodCache = new ConcurrentHashMap, Set>(); + + private final Map, Set> modelAttributeMethodCache = new ConcurrentHashMap, Set>(); + /** * Create a {@link RequestMappingHandlerAdapter} instance. */ @@ -177,10 +177,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i *

Generally custom argument resolvers are invoked first. However this excludes * default argument resolvers that rely on the presence of annotations (e.g. {@code @RequestParameter}, * {@code @PathVariable}, etc.) Those resolvers can only be customized via {@link #setArgumentResolvers(List)} - *

An existing {@link WebArgumentResolver} can either adapted with {@link ServletWebArgumentResolverAdapter} - * or preferably converted to a {@link HandlerMethodArgumentResolver} instead. */ - public void setCustomArgumentResolvers(List argumentResolvers) { + public void setCustomArgumentResolvers(List argumentResolvers) { this.customArgumentResolvers = argumentResolvers; } @@ -190,7 +188,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i * {@link #setCustomArgumentResolvers(List)}, which does not override default registrations. * @param argumentResolvers argument resolvers for {@link RequestMapping} and {@link ModelAttribute} methods */ - public void setArgumentResolvers(List argumentResolvers) { + public void setArgumentResolvers(List argumentResolvers) { if (argumentResolvers != null) { this.argumentResolvers = new HandlerMethodArgumentResolverComposite(); this.argumentResolvers.addResolvers(argumentResolvers); @@ -203,7 +201,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i * {@link #setCustomArgumentResolvers(List)}, which does not override default registrations. * @param argumentResolvers argument resolvers for {@link InitBinder} methods */ - public void setInitBinderArgumentResolvers(List argumentResolvers) { + public void setInitBinderArgumentResolvers(List argumentResolvers) { if (argumentResolvers != null) { this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite(); this.initBinderArgumentResolvers.addResolvers(argumentResolvers); @@ -217,7 +215,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i * and others. Those handlers can only be customized via {@link #setReturnValueHandlers(List)}. * @param returnValueHandlers custom return value handlers for {@link RequestMapping} methods */ - public void setCustomReturnValueHandlers(List returnValueHandlers) { + public void setCustomReturnValueHandlers(List returnValueHandlers) { this.customReturnValueHandlers = returnValueHandlers; } @@ -227,7 +225,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i * {@link #setCustomReturnValueHandlers(List)}, which does not override default registrations. * @param returnValueHandlers the return value handlers for {@link RequestMapping} methods */ - public void setReturnValueHandlers(List returnValueHandlers) { + public void setReturnValueHandlers(List returnValueHandlers) { if (returnValueHandlers != null) { this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite(); this.returnValueHandlers.addHandlers(returnValueHandlers); @@ -236,10 +234,10 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i /** * Set custom {@link ModelAndViewResolver}s to use to handle the return values of {@link RequestMapping} methods. - *

Custom {@link ModelAndViewResolver}s are provided for backward compatibility and are invoked at the very, - * from the {@link DefaultMethodReturnValueHandler}, after all standard {@link HandlerMethodReturnValueHandler}s - * have been given a chance. This is because {@link ModelAndViewResolver}s do not have a method to indicate - * if they support a given return type or not. For this reason it is recommended to use + *

Custom {@link ModelAndViewResolver}s are provided for backward compatibility and are invoked at the end, + * in {@link DefaultMethodReturnValueHandler}, after all standard {@link HandlerMethodReturnValueHandler}s. + * This is because {@link ModelAndViewResolver}s do not have a method to indicate if they support a given + * return type or not. For this reason it is recommended to use * {@link HandlerMethodReturnValueHandler} and {@link #setCustomReturnValueHandlers(List)} instead. */ public void setModelAndViewResolvers(List modelAndViewResolvers) { @@ -443,7 +441,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i } /** - * This method always returns -1 since {@link HandlerMethod} does not implement {@link LastModified}. + * {@inheritDoc} + *

This implementation always returns -1 since {@link HandlerMethod} does not implement {@link LastModified}. * Instead an @{@link RequestMapping} method, calculate the lastModified value, and call * {@link WebRequest#checkNotModified(long)}, and return {@code null} if that returns {@code true}. * @see WebRequest#checkNotModified(long) @@ -510,13 +509,11 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i ServletWebRequest webRequest = new ServletWebRequest(request, response); SessionStatus sessionStatus = new SimpleSessionStatus(); - + ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - modelFactory.initModel(webRequest, mavContainer, requestMethod); requestMethod.invokeAndHandle(webRequest, mavContainer, sessionStatus); - modelFactory.updateModel(webRequest, mavContainer, sessionStatus); if (!mavContainer.isResolveView()) { @@ -548,7 +545,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i binderMethod.setHandlerMethodArgumentResolvers(this.initBinderArgumentResolvers); binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer)); binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); - initBinderMethods.add(binderMethod); } 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 index c1927d83b9..884f01ae4f 100644 --- 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 @@ -38,7 +38,8 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; /** - * A base class for resolving method argument values by reading from the body of a request with {@link HttpMessageConverter}s. + * A base class for resolving method argument values by reading from the body of a request + * with {@link HttpMessageConverter}s. * * @author Arjen Poutsma * @author Rossen Stoyanchev 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 index f8db5a9722..eae6068073 100644 --- 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 @@ -17,17 +17,20 @@ package org.springframework.web.servlet.mvc.method.annotation.support; import java.lang.annotation.Annotation; +import java.util.Collection; import java.util.List; -import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import org.springframework.core.GenericCollectionTypeResolver; 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.ServletRequestBindingException; import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; @@ -35,21 +38,34 @@ import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartRequest; +import org.springframework.web.multipart.MultipartResolver; 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. + * Resolves the following method arguments: + *

    + *
  • Arguments annotated with @{@link RequestPart}. + *
  • Arguments of type {@link MultipartFile} in conjunction with Spring's + * {@link MultipartResolver} abstraction. + *
  • Arguments of type {@code javax.servlet.http.Part} in conjunction + * with Servlet 3.0 multipart requests. + *
* - *

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}. + *

When a parameter is annotated with @{@link RequestPart} the content of the + * part is passed through an {@link HttpMessageConverter} to resolve the method + * argument with the 'Content-Type' of the request part in mind. This is + * analogous to what @{@link RequestBody} does to resolve an argument based on + * the content of a non-multipart request. + * + *

When a parameter is not annotated or the name of the part is not specified, + * it is derived from the name of the method argument. + * + *

Automatic validation can be applied to a @{@link RequestPart} method argument + * through the use of {@code @Valid}. In case of validation failure, a + * {@link RequestPartNotValidException} is thrown and handled automatically through + * the {@link DefaultHandlerExceptionResolver}. * * @author Rossen Stoyanchev * @since 3.1 @@ -60,8 +76,27 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM super(messageConverters); } + /** + * Supports the following: + *

    + *
  • @RequestPart method arguments. + *
  • Arguments of type {@link MultipartFile} even if not annotated. + *
  • Arguments of type {@code javax.servlet.http.Part} even if not annotated. + *
+ */ public boolean supportsParameter(MethodParameter parameter) { - return parameter.hasParameterAnnotation(RequestPart.class); + if (parameter.hasParameterAnnotation(RequestPart.class)) { + return true; + } + else if (MultipartFile.class.equals(parameter.getParameterType())) { + return true; + } + else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) { + return true; + } + else { + return false; + } } public Object resolveArgument(MethodParameter parameter, @@ -69,37 +104,45 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM NativeWebRequest request, WebDataBinderFactory binderFactory) throws Exception { - ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); + String partName = getPartName(parameter); + Object arg; + + HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class); - if (multipartRequest == null) { - throw new IllegalStateException( - "Current request is not of type [" + MultipartRequest.class.getName() + "]: " + request); + + if (MultipartFile.class.equals(parameter.getParameterType())) { + assertMultipartRequest(multipartRequest, request); + arg = multipartRequest.getFile(partName); } - - String partName = getPartName(parameter); - if (MultipartFile.class.isAssignableFrom(parameter.getParameterType())) { - return multipartRequest.getFile(partName); + else if (isMultipartFileCollection(parameter)) { + assertMultipartRequest(multipartRequest, request); + arg = multipartRequest.getFiles(partName); } - - HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(multipartRequest, partName); - Object arg = readWithMessageConverters(inputMessage, parameter, parameter.getParameterType()); - - if (isValidationApplicable(arg, parameter)) { - WebDataBinder binder = binderFactory.createBinder(request, arg, partName); - binder.validate(); - Errors errors = binder.getBindingResult(); - if (errors.hasErrors()) { - throw new RequestPartNotValidException(errors); + else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) { + arg = servletRequest.getPart(partName); + } + else { + HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(multipartRequest, partName); + arg = readWithMessageConverters(inputMessage, parameter, parameter.getParameterType()); + + if (isValidationApplicable(arg, parameter)) { + WebDataBinder binder = binderFactory.createBinder(request, arg, partName); + binder.validate(); + Errors errors = binder.getBindingResult(); + if (errors.hasErrors()) { + throw new RequestPartNotValidException(errors); + } } } - + + checkMissingRequiredValue(arg, partName, parameter); return arg; } private String getPartName(MethodParameter parameter) { RequestPart annot = parameter.getParameterAnnotation(RequestPart.class); - String partName = annot.value(); + String partName = (annot != null) ? annot.value() : ""; if (partName.length() == 0) { partName = parameter.getParameterName(); Assert.notNull(partName, "Request part name for argument type [" + parameter.getParameterType().getName() @@ -108,21 +151,62 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM 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())) { + private void assertMultipartRequest(MultipartHttpServletRequest multipartRequest, NativeWebRequest request) { + if (multipartRequest == null) { + throw new IllegalStateException("Current request is not of type [" + MultipartRequest.class.getName() + + "]: " + request + ". Do you have a MultipartResolver configured?"); + } + } + + private boolean isMultipartFileCollection(MethodParameter parameter) { + Class paramType = parameter.getParameterType(); + if (Collection.class.equals(paramType) || List.class.isAssignableFrom(paramType)){ + Class valueType = GenericCollectionTypeResolver.getCollectionParameterType(parameter); + if (valueType != null && valueType.equals(MultipartFile.class)) { return true; } } return false; } + + /** + * Raises a {@link ServletRequestBindingException} if the method parameter is required + * and the resolved argument value is null. + */ + protected void checkMissingRequiredValue(Object argumentValue, String partName, MethodParameter parameter) + throws ServletRequestBindingException { + if (argumentValue == null) { + RequestPart annot = parameter.getParameterAnnotation(RequestPart.class); + boolean isRequired = (annot != null) ? annot.required() : true; + if (isRequired) { + String paramType = parameter.getParameterType().getName(); + throw new ServletRequestBindingException( + "Missing request part '" + partName + "' for method parameter type [" + paramType + "]"); + } + } + } + + /** + * Whether to validate the given @{@link RequestPart} method argument. + * The default implementation return {@code true} if the argument value is not {@code null} + * and the method parameter is 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) { + if (argumentValue == null) { + return false; + } + else { + 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/RequestResponseBodyMethodProcessor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RequestResponseBodyMethodProcessor.java index 092f2c4b92..20fbdc73d7 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 @@ -24,7 +24,6 @@ import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.validation.Errors; -import org.springframework.validation.Validator; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.RequestBody; @@ -32,17 +31,15 @@ import org.springframework.web.bind.annotation.ResponseBody; 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}. 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}. + *

An @{@link RequestBody} method argument will be validated if annotated with {@code @Valid}. + * In case of validation failure, a {@link RequestBodyNotValidException} is thrown and handled + * automatically in {@link DefaultHandlerExceptionResolver}. * * @author Arjen Poutsma * @author Rossen Stoyanchev diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockHttpServletRequest.java b/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockHttpServletRequest.java index 26f7171b58..fd599c2d23 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockHttpServletRequest.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockHttpServletRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.mock.web; import java.io.BufferedReader; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; @@ -27,6 +28,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Enumeration; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; @@ -34,19 +36,28 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; +import javax.servlet.ServletException; import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import javax.servlet.http.Part; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; /** * Mock implementation of the {@link javax.servlet.http.HttpServletRequest} - * interface. Supports the Servlet 2.5 API level. + * interface. Supports the Servlet 2.5 API level; throws + * {@link UnsupportedOperationException} for all methods introduced in Servlet 3.0. * *

Used for testing the web framework; also useful for testing * application controllers. @@ -55,6 +66,7 @@ import org.springframework.util.LinkedCaseInsensitiveMap; * @author Rod Johnson * @author Rick Evans * @author Mark Fisher + * @author Chris Beams * @since 1.0.2 */ public class MockHttpServletRequest implements HttpServletRequest { @@ -134,6 +146,7 @@ public class MockHttpServletRequest implements HttpServletRequest { private int localPort = DEFAULT_SERVER_PORT; + private Map parts = new HashMap(); //--------------------------------------------------------------------- // HttpServletRequest properties @@ -847,4 +860,57 @@ public class MockHttpServletRequest implements HttpServletRequest { return isRequestedSessionIdFromURL(); } + + //--------------------------------------------------------------------- + // Methods introduced in Servlet 3.0 + //--------------------------------------------------------------------- + + public AsyncContext getAsyncContext() { + throw new UnsupportedOperationException(); + } + + public DispatcherType getDispatcherType() { + throw new UnsupportedOperationException(); + } + + public boolean isAsyncSupported() { + throw new UnsupportedOperationException(); + } + + public AsyncContext startAsync() { + throw new UnsupportedOperationException(); + } + + public AsyncContext startAsync(ServletRequest arg0, ServletResponse arg1) { + throw new UnsupportedOperationException(); + } + + public boolean isAsyncStarted() { + throw new UnsupportedOperationException(); + } + + public boolean authenticate(HttpServletResponse arg0) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + + public void addPart(Part part) { + parts.put(part.getName(), part); + } + + public Part getPart(String key) throws IOException, IllegalStateException, ServletException { + return parts.get(key); + } + + public Collection getParts() throws IOException, IllegalStateException, ServletException { + return parts.values(); + } + + public void login(String arg0, String arg1) throws ServletException { + throw new UnsupportedOperationException(); + } + + public void logout() throws ServletException { + throw new UnsupportedOperationException(); + } + } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockHttpServletResponse.java b/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockHttpServletResponse.java index 46eb174615..9f55159e7a 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockHttpServletResponse.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; + import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; @@ -292,9 +293,9 @@ public class MockHttpServletResponse implements HttpServletResponse { * @param name the name of the header * @return the associated header value, or null if none */ - public Object getHeader(String name) { + public String getHeader(String name) { HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); - return (header != null ? header.getValue() : null); + return (header != null ? header.getValue().toString() : null); } /** @@ -302,9 +303,9 @@ public class MockHttpServletResponse implements HttpServletResponse { * @param name the name of the header * @return the associated header values, or an empty List if none */ - public List getHeaders(String name) { + public List getHeaders(String name) { HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name); - return (header != null ? header.getValues() : Collections.emptyList()); + return (header != null ? header.getStringValues() : Collections.emptyList()); } /** diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockPart.java b/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockPart.java new file mode 100644 index 0000000000..b0221b05b7 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockPart.java @@ -0,0 +1,139 @@ +/* + * 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.mock.web; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; + +import javax.servlet.http.Part; + +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; + +/** + * Mock implementation of the {@link Part} interface. + * + * @author Rossen Stoyanchev + * @since 3.1 + * @see MockHttpServletRequest + */ +public class MockPart implements Part { + + private static final String CONTENT_TYPE = "Content-Type"; + + private final String name; + + private String contentType; + + private final byte[] content; + + /** + * Create a new MockPart with the given content. + * @param name the name of the part + * @param content the content for the part + */ + public MockPart(String name, byte[] content) { + this(name, "", content); + } + + /** + * Create a new MockPart with the given content. + * @param name the name of the part + * @param contentStream the content of the part as stream + * @throws IOException if reading from the stream failed + */ + public MockPart(String name, InputStream contentStream) throws IOException { + this(name, "", FileCopyUtils.copyToByteArray(contentStream)); + } + + /** + * Create a new MockPart with the given content. + * @param name the name of the file + * @param contentType the content type (if known) + * @param content the content of the file + */ + public MockPart(String name, String contentType, byte[] content) { + Assert.hasLength(name, "Name must not be null"); + this.name = name; + this.contentType = contentType; + this.content = (content != null ? content : new byte[0]); + } + + /** + * Create a new MockPart with the given content. + * @param name the name of the file + * @param contentType the content type (if known) + * @param contentStream the content of the part as stream + * @throws IOException if reading from the stream failed + */ + public MockPart(String name, String contentType, InputStream contentStream) + throws IOException { + + this(name, contentType, FileCopyUtils.copyToByteArray(contentStream)); + } + + + public String getName() { + return this.name; + } + + public String getContentType() { + return this.contentType; + } + + public long getSize() { + return this.content.length; + } + + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(this.content); + } + + public String getHeader(String name) { + if (CONTENT_TYPE.equalsIgnoreCase(name)) { + return this.contentType; + } + else { + return null; + } + } + + public Collection getHeaders(String name) { + if (CONTENT_TYPE.equalsIgnoreCase(name)) { + return Collections.singleton(this.contentType); + } + else { + return null; + } + } + + public Collection getHeaderNames() { + return Collections.singleton(CONTENT_TYPE); + } + + public void write(String fileName) throws IOException { + throw new UnsupportedOperationException(); + } + + public void delete() throws IOException { + throw new UnsupportedOperationException(); + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockServletContext.java b/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockServletContext.java index 42af3f71e1..828b430e6c 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockServletContext.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/mock/web/MockServletContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * 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. @@ -23,6 +23,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; import java.util.Enumeration; +import java.util.EventListener; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -30,9 +31,17 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import javax.activation.FileTypeMap; +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.FilterRegistration.Dynamic; import javax.servlet.RequestDispatcher; import javax.servlet.Servlet; import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.descriptor.JspConfigDescriptor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -67,8 +76,12 @@ import org.springframework.web.util.WebUtils; * and XmlWebApplicationContext with an underlying MockServletContext (as long as * the MockServletContext has been configured with a FileSystemResourceLoader). * + * Supports the Servlet 3.0 API level, but throws {@link UnsupportedOperationException} + * for all methods introduced in Servlet 3.0. + * * @author Rod Johnson * @author Juergen Hoeller + * @author Chris Beams * @since 1.0.2 * @see #MockServletContext(org.springframework.core.io.ResourceLoader) * @see org.springframework.web.context.support.XmlWebApplicationContext @@ -89,6 +102,8 @@ public class MockServletContext implements ServletContext { private String contextPath = ""; + private int minorVersion = 5; + private final Map contexts = new HashMap(); private final Map initParameters = new LinkedHashMap(); @@ -180,8 +195,15 @@ public class MockServletContext implements ServletContext { return 2; } + public void setMinorVersion(int minorVersion) { + if (minorVersion < 3 || minorVersion > 5) { + throw new IllegalArgumentException("Only Servlet minor versions between 3 and 5 are supported"); + } + this.minorVersion = minorVersion; + } + public int getMinorVersion() { - return 5; + return this.minorVersion; } public String getMimeType(String filePath) { @@ -352,4 +374,119 @@ public class MockServletContext implements ServletContext { } } + + //--------------------------------------------------------------------- + // Methods introduced in Servlet 3.0 + //--------------------------------------------------------------------- + + public Dynamic addFilter(String arg0, String arg1) { + throw new UnsupportedOperationException(); + } + + public Dynamic addFilter(String arg0, Filter arg1) { + throw new UnsupportedOperationException(); + } + + public Dynamic addFilter(String arg0, Class arg1) { + throw new UnsupportedOperationException(); + } + + public void addListener(Class arg0) { + throw new UnsupportedOperationException(); + } + + public void addListener(String arg0) { + throw new UnsupportedOperationException(); + } + + public void addListener(T arg0) { + throw new UnsupportedOperationException(); + } + + public javax.servlet.ServletRegistration.Dynamic addServlet(String arg0, String arg1) { + throw new UnsupportedOperationException(); + } + + public javax.servlet.ServletRegistration.Dynamic addServlet(String arg0, + Servlet arg1) { + throw new UnsupportedOperationException(); + } + + public javax.servlet.ServletRegistration.Dynamic addServlet(String arg0, + Class arg1) { + throw new UnsupportedOperationException(); + } + + public T createFilter(Class arg0) + throws ServletException { + throw new UnsupportedOperationException(); + } + + public T createListener(Class arg0) + throws ServletException { + throw new UnsupportedOperationException(); + } + + public T createServlet(Class arg0) + throws ServletException { + throw new UnsupportedOperationException(); + } + + public void declareRoles(String... arg0) { + throw new UnsupportedOperationException(); + } + + public ClassLoader getClassLoader() { + throw new UnsupportedOperationException(); + } + + public Set getDefaultSessionTrackingModes() { + throw new UnsupportedOperationException(); + } + + public int getEffectiveMajorVersion() { + throw new UnsupportedOperationException(); + } + + public int getEffectiveMinorVersion() { + throw new UnsupportedOperationException(); + } + + public Set getEffectiveSessionTrackingModes() { + throw new UnsupportedOperationException(); + } + + public FilterRegistration getFilterRegistration(String arg0) { + throw new UnsupportedOperationException(); + } + + public Map getFilterRegistrations() { + throw new UnsupportedOperationException(); + } + + public JspConfigDescriptor getJspConfigDescriptor() { + throw new UnsupportedOperationException(); + } + + public ServletRegistration getServletRegistration(String arg0) { + throw new UnsupportedOperationException(); + } + + public Map getServletRegistrations() { + throw new UnsupportedOperationException(); + } + + public SessionCookieConfig getSessionCookieConfig() { + throw new UnsupportedOperationException(); + } + + public boolean setInitParameter(String arg0, String arg1) { + throw new UnsupportedOperationException(); + } + + public void setSessionTrackingModes(Set arg0) + throws IllegalStateException, IllegalArgumentException { + throw new UnsupportedOperationException(); + } + } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java index b446166eb3..92dbe8729b 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/DispatcherServletTests.java @@ -207,7 +207,7 @@ public class DispatcherServletTests extends TestCase { MockHttpServletResponse response = new MockHttpServletResponse(); simpleDispatcherServlet.service(request, response); assertTrue("Not forwarded", response.getForwardedUrl() == null); - assertEquals(new Long(98), response.getHeader("Last-Modified")); + assertEquals("98", response.getHeader("Last-Modified")); } public void testUnknownRequest() throws Exception { @@ -280,7 +280,7 @@ public class DispatcherServletTests extends TestCase { assertTrue(request.getAttribute("test3") != null); assertTrue(request.getAttribute("test3x") != null); assertTrue(request.getAttribute("test3y") != null); - assertEquals(new Long(99), response.getHeader("Last-Modified")); + assertEquals("99", response.getHeader("Last-Modified")); } public void testExistingMultipartRequest() throws Exception { diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/CommandControllerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/CommandControllerTests.java index 1b757c9314..9540799f2e 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/CommandControllerTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/CommandControllerTests.java @@ -144,7 +144,7 @@ public class CommandControllerTests extends TestCase { HttpServletRequest request = new MockHttpServletRequest("GET", "/ok.html"); MockHttpServletResponse response = new MockHttpServletResponse(); mc.handleRequest(request, response); - assertTrue("Correct expires header", response.getHeader("Expires").equals(new Long(1))); + assertTrue("Correct expires header", response.getHeader("Expires").equals("1")); List cacheControl = response.getHeaders("Cache-Control"); assertTrue("Correct cache control", cacheControl.contains("no-cache")); assertTrue("Correct cache control", cacheControl.contains("no-store")); @@ -170,7 +170,7 @@ public class CommandControllerTests extends TestCase { HttpServletRequest request = new MockHttpServletRequest("GET", "/ok.html"); MockHttpServletResponse response = new MockHttpServletResponse(); mc.handleRequest(request, response); - assertTrue("Correct expires header", response.getHeader("Expires").equals(new Long(1))); + assertTrue("Correct expires header", response.getHeader("Expires").equals("1")); assertTrue("No cache control", response.getHeader("Cache-Control") == null); } @@ -259,7 +259,7 @@ public class CommandControllerTests extends TestCase { HttpServletRequest request = new MockHttpServletRequest("GET", "/ok.html"); MockHttpServletResponse response = new MockHttpServletResponse(); mc.handleRequest(request, response); - assertTrue("Correct expires header", response.getHeader("Expires").equals(new Long(1))); + assertTrue("Correct expires header", response.getHeader("Expires").equals("1")); List cacheControl = response.getHeaders("Cache-Control"); assertTrue("Correct cache control", cacheControl.contains("no-cache")); assertTrue("Correct cache control", cacheControl.contains("no-store")); diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java index ff30727bc2..c36453891a 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java @@ -146,7 +146,7 @@ public class RequestMappingHandlerAdapterTests { @SuppressWarnings("unchecked") public void setCustomArgumentResolvers() { TestHanderMethodArgumentResolver resolver = new TestHanderMethodArgumentResolver(); - handlerAdapter.setCustomArgumentResolvers(Arrays.asList(resolver)); + handlerAdapter.setCustomArgumentResolvers(Arrays.asList(resolver)); handlerAdapter.afterPropertiesSet(); HandlerMethodArgumentResolverComposite composite = (HandlerMethodArgumentResolverComposite) @@ -170,7 +170,7 @@ public class RequestMappingHandlerAdapterTests { @SuppressWarnings("unchecked") public void setCustomReturnValueHandlers() { TestHandlerMethodReturnValueHandler handler = new TestHandlerMethodReturnValueHandler(); - handlerAdapter.setCustomReturnValueHandlers(Arrays.asList(handler)); + handlerAdapter.setCustomReturnValueHandlers(Arrays.asList(handler)); handlerAdapter.afterPropertiesSet(); HandlerMethodReturnValueHandlerComposite composite = (HandlerMethodReturnValueHandlerComposite) 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 index 0a36662303..acafb9f598 100644 --- 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 @@ -23,17 +23,20 @@ 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.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Collections; +import java.util.List; +import javax.servlet.http.Part; import javax.validation.Valid; import javax.validation.constraints.NotNull; @@ -43,10 +46,13 @@ 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.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartHttpServletRequest; +import org.springframework.mock.web.MockPart; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -67,31 +73,42 @@ public class RequestPartMethodArgumentResolverTests { private HttpMessageConverter messageConverter; - private MultipartFile multipartFile; + private MultipartFile multipartFile1; + private MultipartFile multipartFile2; private MethodParameter paramRequestPart; private MethodParameter paramNamedRequestPart; private MethodParameter paramValidRequestPart; private MethodParameter paramMultipartFile; + private MethodParameter paramMultipartFileList; private MethodParameter paramInt; + private MethodParameter paramMultipartFileNotAnnot; + private MethodParameter paramServlet30Part; private NativeWebRequest webRequest; - private MockMultipartHttpServletRequest servletRequest; + private MockMultipartHttpServletRequest multipartRequest; private MockHttpServletResponse servletResponse; @SuppressWarnings("unchecked") @Before public void setUp() throws Exception { - Method handle = getClass().getMethod("handle", - SimpleBean.class, SimpleBean.class, SimpleBean.class, MultipartFile.class, Integer.TYPE); - paramRequestPart = new MethodParameter(handle, 0); + + Method method = getClass().getMethod("handle", SimpleBean.class, SimpleBean.class, SimpleBean.class, + MultipartFile.class, List.class, Integer.TYPE, MultipartFile.class, Part.class); + + paramRequestPart = new MethodParameter(method, 0); paramRequestPart.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); - paramNamedRequestPart = new MethodParameter(handle, 1); - paramValidRequestPart = new MethodParameter(handle, 2); - paramMultipartFile = new MethodParameter(handle, 3); - paramInt = new MethodParameter(handle, 4); + paramNamedRequestPart = new MethodParameter(method, 1); + paramValidRequestPart = new MethodParameter(method, 2); + paramMultipartFile = new MethodParameter(method, 3); + paramMultipartFileList = new MethodParameter(method, 4); + paramInt = new MethodParameter(method, 5); + paramMultipartFileNotAnnot = new MethodParameter(method, 6); + paramMultipartFileNotAnnot.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + paramServlet30Part = new MethodParameter(method, 7); + paramServlet30Part.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); messageConverter = createMock(HttpMessageConverter.class); expect(messageConverter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN)); @@ -100,19 +117,64 @@ public class RequestPartMethodArgumentResolverTests { resolver = new RequestPartMethodArgumentResolver(Collections.>singletonList(messageConverter)); reset(messageConverter); - multipartFile = new MockMultipartFile("requestPart", "", "text/plain", (byte[]) null); - servletRequest = new MockMultipartHttpServletRequest(); - servletRequest.addFile(multipartFile); + multipartFile1 = new MockMultipartFile("requestPart", "", "text/plain", (byte[]) null); + multipartFile2 = new MockMultipartFile("requestPart", "", "text/plain", (byte[]) null); + multipartRequest = new MockMultipartHttpServletRequest(); + multipartRequest.addFile(multipartFile1); + multipartRequest.addFile(multipartFile2); servletResponse = new MockHttpServletResponse(); - webRequest = new ServletWebRequest(servletRequest, servletResponse); + webRequest = new ServletWebRequest(multipartRequest, servletResponse); } @Test public void supportsParameter() { assertTrue("RequestPart parameter not supported", resolver.supportsParameter(paramRequestPart)); + assertTrue("MultipartFile parameter not supported", resolver.supportsParameter(paramMultipartFileNotAnnot)); + assertTrue("Part parameter not supported", resolver.supportsParameter(paramServlet30Part)); assertFalse("non-RequestPart parameter supported", resolver.supportsParameter(paramInt)); } - + + @Test + public void resolveMultipartFile() throws Exception { + Object actual = resolver.resolveArgument(paramMultipartFile, null, webRequest, null); + assertNotNull(actual); + assertSame(multipartFile1, actual); + } + + @Test + public void resolveMultipartFileList() throws Exception { + Object actual = resolver.resolveArgument(paramMultipartFileList, null, webRequest, null); + assertNotNull(actual); + assertTrue(actual instanceof List); + assertEquals(Arrays.asList(multipartFile1, multipartFile2), actual); + } + + @Test + public void resolveMultipartFileNotAnnotArgument() throws Exception { + MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); + MultipartFile expected = new MockMultipartFile("multipartFileNotAnnot", "Hello World".getBytes()); + request.addFile(expected); + webRequest = new ServletWebRequest(request); + + Object result = resolver.resolveArgument(paramMultipartFileNotAnnot, null, webRequest, null); + + assertTrue(result instanceof MultipartFile); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveServlet30PartArgument() throws Exception { + MockPart expected = new MockPart("servlet30Part", "Hello World".getBytes()); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addPart(expected); + webRequest = new ServletWebRequest(request); + + Object result = resolver.resolveArgument(paramServlet30Part, null, webRequest, null); + + assertTrue(result instanceof Part); + assertEquals("Invalid result", expected, result); + } + @Test public void resolveRequestPart() throws Exception { testResolveArgument(new SimpleBean("foo"), paramRequestPart); @@ -122,13 +184,6 @@ public class RequestPartMethodArgumentResolverTests { public void resolveNamedRequestPart() throws Exception { testResolveArgument(new SimpleBean("foo"), paramNamedRequestPart); } - - @Test - public void resolveMultipartFile() throws Exception { - Object actual = resolver.resolveArgument(paramMultipartFile, null, webRequest, null); - assertNotNull(actual); - assertSame(multipartFile, actual); - } @Test public void resolveRequestPartNotValid() throws Exception { @@ -144,12 +199,27 @@ public class RequestPartMethodArgumentResolverTests { @Test public void resolveRequestPartValid() throws Exception { - testResolveArgument(new SimpleBean("foo"), paramValidRequestPart); + testResolveArgument(new SimpleBean("foo"), paramNamedRequestPart); } + @Test + public void resolveRequestPartRequired() throws Exception { + try { + testResolveArgument(null, paramValidRequestPart); + fail("Expected exception"); + } catch (ServletRequestBindingException e) { + assertTrue(e.getMessage().contains("Missing request part")); + } + } + + @Test + public void resolveRequestPartNotRequired() 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()); + multipartRequest.addHeader("Content-Type", contentType.toString()); expect(messageConverter.canRead(SimpleBean.class, contentType)).andReturn(true); expect(messageConverter.read(eq(SimpleBean.class), isA(RequestPartServletServerHttpRequest.class))).andReturn(expectedValue); @@ -164,13 +234,6 @@ public class RequestPartMethodArgumentResolverTests { verify(messageConverter); } - public void handle(@RequestPart SimpleBean requestPart, - @RequestPart("requestPart") SimpleBean namedRequestPart, - @Valid @RequestPart("requestPart") SimpleBean validRequestPart, - @RequestPart("requestPart") MultipartFile multipartFile, - int i) { - } - private static class SimpleBean { @NotNull @@ -195,5 +258,15 @@ public class RequestPartMethodArgumentResolverTests { return dataBinder; } } - + + public void handle(@RequestPart SimpleBean requestPart, + @RequestPart(value="requestPart", required=false) SimpleBean namedRequestPart, + @Valid @RequestPart("requestPart") SimpleBean validRequestPart, + @RequestPart("requestPart") MultipartFile multipartFile, + @RequestPart("requestPart") List multipartFileList, + int i, + MultipartFile multipartFileNotAnnot, + Part servlet30Part) { + } + } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java index 1ac0ff2c25..c9af1a43a0 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java @@ -60,10 +60,11 @@ public class ResourceHttpRequestHandlerTests { handler.handleRequest(request, response); assertEquals("text/css", response.getContentType()); assertEquals(17, response.getContentLength()); - assertTrue(((Long)response.getHeader("Expires")) >= System.currentTimeMillis() - 1000 + (3600 * 1000)); + assertTrue(Long.valueOf(response.getHeader("Expires")) >= System.currentTimeMillis() - 1000 + (3600 * 1000)); assertEquals("max-age=3600, must-revalidate", response.getHeader("Cache-Control")); assertTrue(response.containsHeader("Last-Modified")); - assertEquals(response.getHeader("Last-Modified"), new ClassPathResource("test/foo.css", getClass()).getFile().lastModified()); + assertEquals(Long.valueOf(response.getHeader("Last-Modified")).longValue(), + new ClassPathResource("test/foo.css", getClass()).getFile().lastModified()); assertEquals("h1 { color:red; }", response.getContentAsString()); } @@ -75,10 +76,11 @@ public class ResourceHttpRequestHandlerTests { MockHttpServletResponse response = new MockHttpServletResponse(); handler.handleRequest(request, response); assertEquals("text/html", response.getContentType()); - assertTrue(((Long)response.getHeader("Expires")) >= System.currentTimeMillis() - 1000 + (3600 * 1000)); + assertTrue(Long.valueOf(response.getHeader("Expires")) >= System.currentTimeMillis() - 1000 + (3600 * 1000)); assertEquals("max-age=3600, must-revalidate", response.getHeader("Cache-Control")); assertTrue(response.containsHeader("Last-Modified")); - assertEquals(response.getHeader("Last-Modified"), new ClassPathResource("test/foo.html", getClass()).getFile().lastModified()); + assertEquals(Long.valueOf(response.getHeader("Last-Modified")).longValue(), + new ClassPathResource("test/foo.html", getClass()).getFile().lastModified()); } @Test @@ -90,10 +92,11 @@ public class ResourceHttpRequestHandlerTests { handler.handleRequest(request, response); assertEquals("text/css", response.getContentType()); assertEquals(17, response.getContentLength()); - assertTrue(((Long)response.getHeader("Expires")) >= System.currentTimeMillis() - 1000 + (3600 * 1000)); + assertTrue(Long.valueOf(response.getHeader("Expires")) >= System.currentTimeMillis() - 1000 + (3600 * 1000)); assertEquals("max-age=3600, must-revalidate", response.getHeader("Cache-Control")); assertTrue(response.containsHeader("Last-Modified")); - assertEquals(response.getHeader("Last-Modified"), new ClassPathResource("testalternatepath/baz.css", getClass()).getFile().lastModified()); + assertEquals(Long.valueOf(response.getHeader("Last-Modified")).longValue(), + new ClassPathResource("testalternatepath/baz.css", getClass()).getFile().lastModified()); assertEquals("h1 { color:red; }", response.getContentAsString()); } diff --git a/org.springframework.web/.classpath b/org.springframework.web/.classpath index 27dcb7c53c..e74ad26157 100644 --- a/org.springframework.web/.classpath +++ b/org.springframework.web/.classpath @@ -5,11 +5,6 @@ - - - - - @@ -39,5 +34,10 @@ + + + + + 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 1c2610d0c6..6ea18e3084 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 @@ -22,9 +22,15 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.http.converter.HttpMessageConverter; + /** - * Annotation which indicates that a method parameter should be bound to the web request body. Supported for annotated - * handler methods in Servlet environments. + * Annotation indicating a method parameter should be bound to the body of the web request. + * The body of the request is passed through an {@link HttpMessageConverter} to resolve the + * method argument depending on the content type of the request. Optionally, automatic + * validation can be applied by annotating the argument with {@code @Valid}. + * + *

Supported for annotated handler methods in Servlet environments. * * @author Arjen Poutsma * @see RequestHeader 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 index 4a7785b79a..968e970aee 100644 --- 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 @@ -16,20 +16,44 @@ package org.springframework.web.bind.annotation; +import java.beans.PropertyEditor; 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; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartResolver; + /** - * 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. - * + * Annotation that can be used to associate the part of a "multipart/form-data" request + * with a method argument. Supported method argument types include {@link MultipartFile} + * in conjunction with Spring's {@link MultipartResolver} abstraction, + * {@code javax.servlet.http.Part} in conjunction with Servlet 3.0 multipart requests, + * or otherwise for any other method argument, the content of the part is passed through an + * {@link HttpMessageConverter} taking into consideration the 'Content-Type' header + * of the request part. This is analogous to what @{@link RequestBody} does to resolve + * an argument based on the content of a non-multipart regular request. + * + *

Note that @{@link RequestParam} annotation can also be used to associate the + * part of a "multipart/form-data" request with a method argument supporting the same + * method argument types. The main difference is that when the method argument is not a + * String, @{@link RequestParam} relies on type conversion via a registered + * {@link Converter} or {@link PropertyEditor} while @{@link RequestPart} relies + * on {@link HttpMessageConverter}s taking into consideration the 'Content-Type' header + * of the request part. @{@link RequestParam} is likely to be used with name-value form + * fields while @{@link RequestPart} is likely to be used with parts containing more + * complex content (e.g. JSON, XML). + * * @author Rossen Stoyanchev * @author Arjen Poutsma - * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter * @since 3.1 + * + * @see RequestParam + * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @@ -41,4 +65,12 @@ public @interface RequestPart { */ String value() default ""; + /** + * Whether the part is required. + *

Default is true, leading to an exception thrown in case + * of the part missing in the request. Switch this to false + * if you prefer a null in case of the part missing. + */ + boolean required() default true; + } diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolver.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolver.java index 792d002ba9..370dc478b9 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolver.java +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolver.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @@ -33,19 +34,25 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ValueConstants; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.multipart.MultipartRequest; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.util.WebUtils; /** - * Resolves method arguments annotated with @{@link RequestParam}. + * Resolves method arguments annotated with @{@link RequestParam}, arguments of + * type {@link MultipartFile} in conjunction with Spring's {@link MultipartResolver} + * abstraction, and arguments of type {@code javax.servlet.http.Part} in conjunction + * with Servlet 3.0 multipart requests. This resolver can also be created in default + * resolution mode in which simple types (int, long, etc.) not annotated + * with @{@link RequestParam} are also treated as request parameters with the + * parameter name derived from the argument name. * - *

If the method parameter type is {@link Map}, the request parameter name is resolved and then converted - * to a {@link Map} via type conversion assuming a suitable {@link PropertyEditor} or {@link Converter} is - * registered. Alternatively, see {@link RequestParamMapMethodArgumentResolver} for access to all request - * parameters in a {@link Map}. - * - *

If this class is created with default resolution mode on, simple types not annotated - * with @{@link RequestParam} are also treated as request parameters with the parameter name based - * on the method argument name. See the class constructor for more details. + *

If the method parameter type is {@link Map}, the request parameter name is used to + * resolve the request parameter String value. The value is then converted to a {@link Map} + * via type conversion assuming a suitable {@link Converter} or {@link PropertyEditor} has + * been registered. If a request parameter name is not specified with a {@link Map} method + * parameter type, the {@link RequestParamMapMethodArgumentResolver} is used instead + * providing access to all request parameters in the form of a map. * *

A {@link WebDataBinder} is invoked to apply type conversion to resolved request header values that * don't yet match the method parameter type. @@ -72,6 +79,18 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod this.useDefaultResolution = useDefaultResolution; } + /** + * Supports the following: + *

    + *
  • @RequestParam method arguments. This excludes the case where a parameter is of type + * {@link Map} and the annotation does not specify a request parameter name. See + * {@link RequestParamMapMethodArgumentResolver} instead for such parameters. + *
  • Arguments of type {@link MultipartFile} even if not annotated. + *
  • Arguments of type {@code javax.servlet.http.Part} even if not annotated. + *
+ * + *

In default resolution mode, simple type arguments not annotated with @RequestParam are also supported. + */ public boolean supportsParameter(MethodParameter parameter) { Class paramType = parameter.getParameterType(); RequestParam requestParamAnnot = parameter.getParameterAnnotation(RequestParam.class); @@ -81,6 +100,12 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod } return true; } + else if (MultipartFile.class.equals(paramType)) { + return true; + } + else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) { + return true; + } else if (this.useDefaultResolution) { return BeanUtils.isSimpleProperty(paramType); } @@ -99,14 +124,22 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod @Override protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception { - MultipartRequest multipartRequest = webRequest.getNativeRequest(MultipartRequest.class); + + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + MultipartHttpServletRequest multipartRequest = + WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class); + if (multipartRequest != null) { List files = multipartRequest.getFiles(name); if (!files.isEmpty()) { return (files.size() == 1 ? files.get(0) : files); } } - + + if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) { + return servletRequest.getPart(name); + } + String[] paramValues = webRequest.getParameterValues(name); if (paramValues != null) { return paramValues.length == 1 ? paramValues[0] : paramValues; diff --git a/org.springframework.web/src/test/java/org/springframework/mock/web/MockHttpServletRequest.java b/org.springframework.web/src/test/java/org/springframework/mock/web/MockHttpServletRequest.java index c8ea4249a7..fd599c2d23 100644 --- a/org.springframework.web/src/test/java/org/springframework/mock/web/MockHttpServletRequest.java +++ b/org.springframework.web/src/test/java/org/springframework/mock/web/MockHttpServletRequest.java @@ -28,6 +28,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Enumeration; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; @@ -145,6 +146,7 @@ public class MockHttpServletRequest implements HttpServletRequest { private int localPort = DEFAULT_SERVER_PORT; + private Map parts = new HashMap(); //--------------------------------------------------------------------- // HttpServletRequest properties @@ -890,13 +892,17 @@ public class MockHttpServletRequest implements HttpServletRequest { public boolean authenticate(HttpServletResponse arg0) throws IOException, ServletException { throw new UnsupportedOperationException(); } + + public void addPart(Part part) { + parts.put(part.getName(), part); + } - public Part getPart(String arg0) throws IOException, IllegalStateException, ServletException { - throw new UnsupportedOperationException(); + public Part getPart(String key) throws IOException, IllegalStateException, ServletException { + return parts.get(key); } public Collection getParts() throws IOException, IllegalStateException, ServletException { - throw new UnsupportedOperationException(); + return parts.values(); } public void login(String arg0, String arg1) throws ServletException { diff --git a/org.springframework.web/src/test/java/org/springframework/mock/web/MockPart.java b/org.springframework.web/src/test/java/org/springframework/mock/web/MockPart.java new file mode 100644 index 0000000000..b0221b05b7 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/mock/web/MockPart.java @@ -0,0 +1,139 @@ +/* + * 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.mock.web; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; + +import javax.servlet.http.Part; + +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; + +/** + * Mock implementation of the {@link Part} interface. + * + * @author Rossen Stoyanchev + * @since 3.1 + * @see MockHttpServletRequest + */ +public class MockPart implements Part { + + private static final String CONTENT_TYPE = "Content-Type"; + + private final String name; + + private String contentType; + + private final byte[] content; + + /** + * Create a new MockPart with the given content. + * @param name the name of the part + * @param content the content for the part + */ + public MockPart(String name, byte[] content) { + this(name, "", content); + } + + /** + * Create a new MockPart with the given content. + * @param name the name of the part + * @param contentStream the content of the part as stream + * @throws IOException if reading from the stream failed + */ + public MockPart(String name, InputStream contentStream) throws IOException { + this(name, "", FileCopyUtils.copyToByteArray(contentStream)); + } + + /** + * Create a new MockPart with the given content. + * @param name the name of the file + * @param contentType the content type (if known) + * @param content the content of the file + */ + public MockPart(String name, String contentType, byte[] content) { + Assert.hasLength(name, "Name must not be null"); + this.name = name; + this.contentType = contentType; + this.content = (content != null ? content : new byte[0]); + } + + /** + * Create a new MockPart with the given content. + * @param name the name of the file + * @param contentType the content type (if known) + * @param contentStream the content of the part as stream + * @throws IOException if reading from the stream failed + */ + public MockPart(String name, String contentType, InputStream contentStream) + throws IOException { + + this(name, contentType, FileCopyUtils.copyToByteArray(contentStream)); + } + + + public String getName() { + return this.name; + } + + public String getContentType() { + return this.contentType; + } + + public long getSize() { + return this.content.length; + } + + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(this.content); + } + + public String getHeader(String name) { + if (CONTENT_TYPE.equalsIgnoreCase(name)) { + return this.contentType; + } + else { + return null; + } + } + + public Collection getHeaders(String name) { + if (CONTENT_TYPE.equalsIgnoreCase(name)) { + return Collections.singleton(this.contentType); + } + else { + return null; + } + } + + public Collection getHeaderNames() { + return Collections.singleton(CONTENT_TYPE); + } + + public void write(String fileName) throws IOException { + throw new UnsupportedOperationException(); + } + + public void delete() throws IOException { + throw new UnsupportedOperationException(); + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolverTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolverTests.java index 94368d00c1..a0a88b1118 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolverTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/support/RequestParamMethodArgumentResolverTests.java @@ -24,6 +24,8 @@ import static org.junit.Assert.assertTrue; import java.lang.reflect.Method; import java.util.Map; +import javax.servlet.http.Part; + import org.junit.Before; import org.junit.Test; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; @@ -32,6 +34,7 @@ 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.mock.web.MockPart; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.context.request.NativeWebRequest; @@ -54,6 +57,8 @@ public class RequestParamMethodArgumentResolverTests { private MethodParameter paramMultiPartFile; private MethodParameter paramMap; private MethodParameter paramStringNotAnnot; + private MethodParameter paramMultipartFileNotAnnot; + private MethodParameter paramPartNotAnnot; private NativeWebRequest webRequest; @@ -63,8 +68,8 @@ public class RequestParamMethodArgumentResolverTests { public void setUp() throws Exception { resolver = new RequestParamMethodArgumentResolver(null, true); - Method method = getClass().getMethod("params", - String.class, String[].class, Map.class, MultipartFile.class, Map.class, String.class); + Method method = getClass().getMethod("params", String.class, String[].class, Map.class, MultipartFile.class, + Map.class, String.class, MultipartFile.class, Part.class); paramNamedDefaultValueString = new MethodParameter(method, 0); paramNamedStringArray = new MethodParameter(method, 1); @@ -73,6 +78,10 @@ public class RequestParamMethodArgumentResolverTests { paramMap = new MethodParameter(method, 4); paramStringNotAnnot = new MethodParameter(method, 5); paramStringNotAnnot.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + paramMultipartFileNotAnnot = new MethodParameter(method, 6); + paramMultipartFileNotAnnot.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + paramPartNotAnnot = new MethodParameter(method, 7); + paramPartNotAnnot.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); request = new MockHttpServletRequest(); webRequest = new ServletWebRequest(request, new MockHttpServletResponse()); @@ -87,6 +96,8 @@ public class RequestParamMethodArgumentResolverTests { assertTrue("MultipartFile parameter not supported", resolver.supportsParameter(paramMultiPartFile)); assertFalse("non-@RequestParam parameter supported", resolver.supportsParameter(paramMap)); assertTrue("Simple type params supported w/o annotations", resolver.supportsParameter(paramStringNotAnnot)); + assertTrue("MultipartFile parameter not supported", resolver.supportsParameter(paramMultipartFileNotAnnot)); + assertTrue("Part parameter not supported", resolver.supportsParameter(paramPartNotAnnot)); resolver = new RequestParamMethodArgumentResolver(null, false); assertFalse(resolver.supportsParameter(paramStringNotAnnot)); @@ -127,6 +138,32 @@ public class RequestParamMethodArgumentResolverTests { assertEquals("Invalid result", expected, result); } + @Test + public void resolveMultipartFileNotAnnotArgument() throws Exception { + MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); + MultipartFile expected = new MockMultipartFile("paramMultipartFileNotAnnot", "Hello World".getBytes()); + request.addFile(expected); + webRequest = new ServletWebRequest(request); + + Object result = resolver.resolveArgument(paramMultipartFileNotAnnot, null, webRequest, null); + + assertTrue(result instanceof MultipartFile); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolvePartArgument() throws Exception { + MockPart expected = new MockPart("paramPartNotAnnot", "Hello World".getBytes()); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addPart(expected); + webRequest = new ServletWebRequest(request); + + Object result = resolver.resolveArgument(paramPartNotAnnot, null, webRequest, null); + + assertTrue(result instanceof Part); + assertEquals("Invalid result", expected, result); + } + @Test public void resolveDefaultValue() throws Exception { Object result = resolver.resolveArgument(paramNamedDefaultValueString, null, webRequest, null); @@ -157,7 +194,9 @@ public class RequestParamMethodArgumentResolverTests { @RequestParam("name") Map param3, @RequestParam(value = "file") MultipartFile param4, @RequestParam Map param5, - String paramStringNotAnnot) { + String paramStringNotAnnot, + MultipartFile paramMultipartFileNotAnnot, + Part paramPartNotAnnot) { } }