diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java index 171575693a3..f3dbbfb14bc 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -27,17 +27,16 @@ import java.util.HashMap; import java.util.Map; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** - * Helper class that encapsulates the specification of a method parameter, i.e. - * a {@link Method} or {@link Constructor} plus a parameter index and a nested - * type index for a declared generic type. Useful as a specification object to - * pass along. + * Helper class that encapsulates the specification of a method parameter, i.e. a {@link Method} + * or {@link Constructor} plus a parameter index and a nested type index for a declared generic + * type. Useful as a specification object to pass along. * - *

As of 4.2, there is a {@link org.springframework.core.annotation.SynthesizingMethodParameter - * SynthesizingMethodParameter} subclass available which synthesizes annotations - * with attribute aliases. That subclass is used for web and message endpoint - * processing, in particular. + *

As of 4.2, there is a {@link org.springframework.core.annotation.SynthesizingMethodParameter} + * subclass available which synthesizes annotations with attribute aliases. That subclass is used + * for web and message endpoint processing, in particular. * * @author Juergen Hoeller * @author Rob Harrop @@ -49,6 +48,18 @@ import org.springframework.util.Assert; */ public class MethodParameter { + private static Class javaUtilOptionalClass = null; + + static { + try { + javaUtilOptionalClass = ClassUtils.forName("java.util.Optional", MethodParameter.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Java 8 not available - Optional references simply not supported then. + } + } + + private final Method method; private final Constructor constructor; @@ -72,6 +83,8 @@ public class MethodParameter { private volatile String parameterName; + private volatile MethodParameter nestedMethodParameter; + /** * Create a new {@code MethodParameter} for the given method, with nesting level 1. @@ -279,6 +292,44 @@ public class MethodParameter { return this.typeIndexesPerLevel; } + /** + * Return a variant of this {@code MethodParameter} which points to the + * same parameter but one nesting level deeper. This is effectively the + * same as {@link #increaseNestingLevel()}, just with an independent + * {@code MethodParameter} object (e.g. in case of the original being cached). + * @since 4.3 + */ + public MethodParameter nested() { + if (this.nestedMethodParameter != null) { + return this.nestedMethodParameter; + } + MethodParameter nestedParam = clone(); + nestedParam.nestingLevel = this.nestingLevel + 1; + this.nestedMethodParameter = nestedParam; + return nestedParam; + } + + /** + * Return whether this method parameter is declared as optiona + * in the form of Java 8's {@link java.util.Optional}. + * @since 4.3 + */ + public boolean isOptional() { + return (getParameterType() == javaUtilOptionalClass); + } + + /** + * Return a variant of this {@code MethodParameter} which points to + * the same parameter but one nesting level deeper in case of a + * {@link java.util.Optional} declaration. + * @since 4.3 + * @see #isOptional() + * @see #nested() + */ + public MethodParameter nestedIfOptional() { + return (isOptional() ? nested() : this); + } + /** * Set a containing class to resolve the parameter type against. @@ -370,8 +421,8 @@ public class MethodParameter { /** * Return the nested generic type of the method/constructor parameter. * @return the parameter type (never {@code null}) - * @see #getNestingLevel() * @since 4.2 + * @see #getNestingLevel() */ public Type getNestedGenericParameterType() { if (this.nestingLevel > 1) { @@ -526,6 +577,11 @@ public class MethodParameter { return (getMember().hashCode() * 31 + this.parameterIndex); } + @Override + public MethodParameter clone() { + return new MethodParameter(this); + } + /** * Create a new MethodParameter for the given method or constructor. diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizingMethodParameter.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizingMethodParameter.java index a6915e76e95..ea5bb83189a 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizingMethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizingMethodParameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -44,6 +44,10 @@ public class SynthesizingMethodParameter extends MethodParameter { super(method, parameterIndex); } + protected SynthesizingMethodParameter(SynthesizingMethodParameter original) { + super(original); + } + @Override protected A adaptAnnotation(A annotation) { @@ -55,4 +59,10 @@ public class SynthesizingMethodParameter extends MethodParameter { return AnnotationUtils.synthesizeAnnotationArray(annotations, getAnnotatedElement()); } + + @Override + public SynthesizingMethodParameter clone() { + return new SynthesizingMethodParameter(this); + } + } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java index b7898d3ef4a..55b39b8d416 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -265,6 +265,10 @@ public class HandlerMethod { super(HandlerMethod.this.bridgedMethod, index); } + protected HandlerMethodParameter(HandlerMethodParameter original) { + super(original); + } + @Override public Class getContainingClass() { return HandlerMethod.this.getBeanType(); @@ -274,6 +278,11 @@ public class HandlerMethod { public T getMethodAnnotation(Class annotationType) { return HandlerMethod.this.getMethodAnnotation(annotationType); } + + @Override + public HandlerMethodParameter clone() { + return new HandlerMethodParameter(this); + } } @@ -289,10 +298,20 @@ public class HandlerMethod { this.returnValue = returnValue; } + protected ReturnValueMethodParameter(ReturnValueMethodParameter original) { + super(original); + this.returnValue = original.returnValue; + } + @Override public Class getParameterType() { return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType()); } + + @Override + public ReturnValueMethodParameter clone() { + return new ReturnValueMethodParameter(this); + } } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java index f9b9444df6f..eca61cd403b 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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. @@ -84,24 +84,24 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle @Override public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { - Class paramType = parameter.getParameterType(); NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); + MethodParameter nestedParameter = parameter.nestedIfOptional(); - Object arg = resolveArgumentInternal(parameter, message, namedValueInfo.name); + Object arg = resolveArgumentInternal(nestedParameter, message, namedValueInfo.name); if (arg == null) { if (namedValueInfo.defaultValue != null) { arg = resolveDefaultValue(namedValueInfo.defaultValue); } - else if (namedValueInfo.required && !parameter.getParameterType().getName().equals("java.util.Optional")) { - handleMissingValue(namedValueInfo.name, parameter, message); + else if (namedValueInfo.required && !nestedParameter.isOptional()) { + handleMissingValue(namedValueInfo.name, nestedParameter, message); } - arg = handleNullValue(namedValueInfo.name, arg, paramType); + arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); } else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveDefaultValue(namedValueInfo.defaultValue); } - if (!ClassUtils.isAssignableValue(paramType, arg)) { + if (!ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) { arg = this.conversionService.convert( arg, TypeDescriptor.valueOf(arg.getClass()), new TypeDescriptor(parameter)); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DestinationVariableMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DestinationVariableMethodArgumentResolver.java index 9ebd81363ae..76e6a8e1bfa 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DestinationVariableMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DestinationVariableMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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. @@ -42,6 +42,7 @@ public class DestinationVariableMethodArgumentResolver extends AbstractNamedValu super(cs, null); } + @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(DestinationVariable.class); @@ -58,10 +59,9 @@ public class DestinationVariableMethodArgumentResolver extends AbstractNamedValu throws Exception { @SuppressWarnings("unchecked") - Map vars = (Map) message.getHeaders().get( - DESTINATION_TEMPLATE_VARIABLES_HEADER); - - return (vars != null) ? vars.get(name) : null; + Map vars = + (Map) message.getHeaders().get(DESTINATION_TEMPLATE_VARIABLES_HEADER); + return (vars != null ? vars.get(name) : null); } @Override @@ -77,4 +77,5 @@ public class DestinationVariableMethodArgumentResolver extends AbstractNamedValu super(annotation.value(), true, ValueConstants.DEFAULT_NONE); } } -} \ No newline at end of file + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java index 81585d7774d..83c96c3aed5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -272,6 +272,12 @@ public class InvocableHandlerMethod extends HandlerMethod { this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(0); } + protected AsyncResultMethodParameter(AsyncResultMethodParameter original) { + super(original); + this.returnValue = original.returnValue; + this.returnType = original.returnType; + } + @Override public Class getParameterType() { if (this.returnValue != null) { @@ -287,6 +293,11 @@ public class InvocableHandlerMethod extends HandlerMethod { public Type getGenericParameterType() { return this.returnType.getType(); } + + @Override + public AsyncResultMethodParameter clone() { + return new AsyncResultMethodParameter(this); + } } } diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestPart.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestPart.java index b8bd5c28fa0..8f27d5c86fa 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestPart.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestPart.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -55,7 +55,6 @@ import org.springframework.web.multipart.MultipartResolver; * @author Arjen Poutsma * @author Sam Brannen * @since 3.1 - * * @see RequestParam * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter */ diff --git a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java index 33a9b291bbf..3ddde8915a0 100644 --- a/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -280,6 +280,10 @@ public class HandlerMethod { super(HandlerMethod.this.bridgedMethod, index); } + protected HandlerMethodParameter(HandlerMethodParameter original) { + super(original); + } + @Override public Class getContainingClass() { return HandlerMethod.this.getBeanType(); @@ -289,6 +293,11 @@ public class HandlerMethod { public T getMethodAnnotation(Class annotationType) { return HandlerMethod.this.getMethodAnnotation(annotationType); } + + @Override + public HandlerMethodParameter clone() { + return new HandlerMethodParameter(this); + } } @@ -304,10 +313,20 @@ public class HandlerMethod { this.returnValue = returnValue; } + protected ReturnValueMethodParameter(ReturnValueMethodParameter original) { + super(original); + this.returnValue = original.returnValue; + } + @Override public Class getParameterType() { return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType()); } + + @Override + public ReturnValueMethodParameter clone() { + return new ReturnValueMethodParameter(this); + } } } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java index aa724396615..9d9383679d5 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractCookieValueMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -63,7 +63,7 @@ public abstract class AbstractCookieValueMethodArgumentResolver extends Abstract @Override protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException { throw new ServletRequestBindingException("Missing cookie '" + name + - "' for method parameter of type " + parameter.getParameterType().getSimpleName()); + "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName()); } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java index 78a3a345006..909cdad292a 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -26,6 +26,7 @@ import org.springframework.beans.factory.config.BeanExpressionContext; import org.springframework.beans.factory.config.BeanExpressionResolver; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.MethodParameter; +import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ValueConstants; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -53,6 +54,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.1 */ public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver { @@ -61,7 +63,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle private final BeanExpressionContext expressionContext; - private Map namedValueInfoCache = new ConcurrentHashMap(256); + private final Map namedValueInfoCache = new ConcurrentHashMap(256); public AbstractNamedValueMethodArgumentResolver() { @@ -84,18 +86,18 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - Class paramType = parameter.getParameterType(); NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); + MethodParameter nestedParameter = parameter.nestedIfOptional(); - Object arg = resolveName(namedValueInfo.name, parameter, webRequest); + Object arg = resolveName(namedValueInfo.name, nestedParameter, webRequest); if (arg == null) { if (namedValueInfo.defaultValue != null) { arg = resolveDefaultValue(namedValueInfo.defaultValue); } - else if (namedValueInfo.required && !parameter.getParameterType().getName().equals("java.util.Optional")) { - handleMissingValue(namedValueInfo.name, parameter); + else if (namedValueInfo.required && !nestedParameter.isOptional()) { + handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); } - arg = handleNullValue(namedValueInfo.name, arg, paramType); + arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); } else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveDefaultValue(namedValueInfo.defaultValue); @@ -104,7 +106,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); try { - arg = binder.convertIfNecessary(arg, paramType, parameter); + arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } catch (ConversionNotSupportedException ex) { throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(), @@ -151,7 +153,8 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle if (info.name.length() == 0) { name = parameter.getParameterName(); if (name == null) { - throw new IllegalArgumentException("Name for argument type [" + parameter.getParameterType().getName() + + throw new IllegalArgumentException( + "Name for argument type [" + parameter.getNestedParameterType().getName() + "] not available, and parameter name information not found in class file either."); } } @@ -160,18 +163,19 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle } /** - * Resolves the given parameter type and value name into an argument value. + * Resolve the given parameter type and value name into an argument value. * @param name the name of the value being resolved * @param parameter the method parameter to resolve to an argument value + * (pre-nested in case of a {@link java.util.Optional} declaration) * @param request the current request - * @return the resolved argument. May be {@code null} + * @return the resolved argument (may be {@code null}) * @throws Exception in case of errors */ protected abstract Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception; /** - * Resolves the given default value into an argument value. + * Resolve the given default value into an argument value. */ private Object resolveDefaultValue(String defaultValue) { if (this.configurableBeanFactory == null) { @@ -190,8 +194,23 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle * returned {@code null} and there is no default value. Subclasses typically throw an exception in this case. * @param name the name for the value * @param parameter the method parameter + * @param request the current request + * @since 4.3 */ - protected abstract void handleMissingValue(String name, MethodParameter parameter) throws ServletException; + protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + handleMissingValue(name, parameter); + } + + /** + * Invoked when a named value is required, but {@link #resolveName(String, MethodParameter, NativeWebRequest)} + * returned {@code null} and there is no default value. Subclasses typically throw an exception in this case. + * @param name the name for the value + * @param parameter the method parameter + */ + protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException { + throw new ServletRequestBindingException("Missing argument '" + name + + "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName()); + } /** * A {@code null} results in a {@code false} value for {@code boolean}s or an exception for other primitives. @@ -202,7 +221,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle return Boolean.FALSE; } else if (paramType.isPrimitive()) { - throw new IllegalStateException("Optional " + paramType + " parameter '" + name + + throw new IllegalStateException("Optional " + paramType.getSimpleName() + " parameter '" + name + "' is present but cannot be translated into a null value due to being declared as a " + "primitive type. Consider declaring it as object wrapper for the corresponding primitive type."); } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java index 3ef1b4fea83..c8575251cea 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java @@ -56,7 +56,7 @@ public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMetho @Override public boolean supportsParameter(MethodParameter parameter) { return (parameter.hasParameterAnnotation(RequestHeader.class) && - !Map.class.isAssignableFrom(parameter.getParameterType())); + !Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())); } @Override @@ -79,7 +79,7 @@ public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMetho @Override protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException { throw new ServletRequestBindingException("Missing request header '" + name + - "' for method parameter of type " + parameter.getParameterType().getSimpleName()); + "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName()); } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java index e2671f3b081..24bdb56cc4b 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -17,22 +17,17 @@ package org.springframework.web.method.annotation; import java.beans.PropertyEditor; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.Part; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.WebDataBinder; @@ -45,6 +40,8 @@ import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartResolver; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.multipart.support.MultipartResolutionDelegate; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.WebUtils; @@ -124,9 +121,8 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod */ @Override public boolean supportsParameter(MethodParameter parameter) { - Class paramType = parameter.getParameterType(); if (parameter.hasParameterAnnotation(RequestParam.class)) { - if (Map.class.isAssignableFrom(paramType)) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { String paramName = parameter.getParameterAnnotation(RequestParam.class).name(); return StringUtils.hasText(paramName); } @@ -138,11 +134,12 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod if (parameter.hasParameterAnnotation(RequestPart.class)) { return false; } - else if (MultipartFile.class == paramType || "javax.servlet.http.Part".equals(paramType.getName())) { + parameter = parameter.nestedIfOptional(); + if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { return true; } else if (this.useDefaultResolution) { - return BeanUtils.isSimpleProperty(paramType); + return BeanUtils.isSimpleProperty(parameter.getNestedParameterType()); } else { return false; @@ -157,112 +154,61 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod } @Override - protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception { - HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class); - Object arg; - if (MultipartFile.class == parameter.getParameterType()) { - assertIsMultipartRequest(servletRequest); - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - arg = multipartRequest.getFile(name); - } - else if (isMultipartFileCollection(parameter)) { - assertIsMultipartRequest(servletRequest); - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - arg = multipartRequest.getFiles(name); - } - else if (isMultipartFileArray(parameter)) { - assertIsMultipartRequest(servletRequest); - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - List multipartFiles = multipartRequest.getFiles(name); - arg = multipartFiles.toArray(new MultipartFile[multipartFiles.size()]); - } - else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) { - assertIsMultipartRequest(servletRequest); - arg = servletRequest.getPart(name); - } - else if (isPartCollection(parameter)) { - assertIsMultipartRequest(servletRequest); - arg = new ArrayList(servletRequest.getParts()); - } - else if (isPartArray(parameter)) { - assertIsMultipartRequest(servletRequest); - arg = RequestPartResolver.resolvePart(servletRequest); - } - else { - arg = null; - if (multipartRequest != null) { - List files = multipartRequest.getFiles(name); - if (!files.isEmpty()) { - arg = (files.size() == 1 ? files.get(0) : files); - } - } - if (arg == null) { - String[] paramValues = webRequest.getParameterValues(name); - if (paramValues != null) { - arg = (paramValues.length == 1 ? paramValues[0] : paramValues); - } - } + Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); + if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { + return mpArg; } + Object arg = null; + if (multipartRequest != null) { + List files = multipartRequest.getFiles(name); + if (!files.isEmpty()) { + arg = (files.size() == 1 ? files.get(0) : files); + } + } + if (arg == null) { + String[] paramValues = request.getParameterValues(name); + if (paramValues != null) { + arg = (paramValues.length == 1 ? paramValues[0] : paramValues); + } + } return arg; } - private void assertIsMultipartRequest(HttpServletRequest request) { - String contentType = request.getContentType(); - if (contentType == null || !contentType.toLowerCase().startsWith("multipart/")) { - throw new MultipartException("The current request is not a multipart request"); - } - } - - private boolean isMultipartFileCollection(MethodParameter parameter) { - return (MultipartFile.class == getCollectionParameterType(parameter)); - } - - private boolean isMultipartFileArray(MethodParameter parameter) { - return (MultipartFile.class == parameter.getParameterType().getComponentType()); - } - - private boolean isPartCollection(MethodParameter parameter) { - Class collectionType = getCollectionParameterType(parameter); - return (collectionType != null && "javax.servlet.http.Part".equals(collectionType.getName())); - } - - private boolean isPartArray(MethodParameter parameter) { - Class paramType = parameter.getParameterType().getComponentType(); - return (paramType != null && "javax.servlet.http.Part".equals(paramType.getName())); - } - - private Class getCollectionParameterType(MethodParameter parameter) { - Class paramType = parameter.getParameterType(); - if (Collection.class == paramType || List.class.isAssignableFrom(paramType)){ - Class valueType = GenericCollectionTypeResolver.getCollectionParameterType(parameter); - if (valueType != null) { - return valueType; + @Override + protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); + if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { + if (!MultipartResolutionDelegate.isMultipartRequest(servletRequest)) { + throw new MultipartException("Current request is not a multipart request"); + } + else { + throw new MissingServletRequestPartException(name); } } - return null; - } - - @Override - protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException { - throw new MissingServletRequestParameterException(name, parameter.getParameterType().getSimpleName()); + else { + throw new MissingServletRequestParameterException(name, parameter.getNestedParameterType().getSimpleName()); + } } @Override public void contributeMethodArgument(MethodParameter parameter, Object value, UriComponentsBuilder builder, Map uriVariables, ConversionService conversionService) { - Class paramType = parameter.getParameterType(); + Class paramType = parameter.getNestedParameterType(); if (Map.class.isAssignableFrom(paramType) || MultipartFile.class == paramType || "javax.servlet.http.Part".equals(paramType.getName())) { return; } RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); - String name = (requestParam == null || StringUtils.isEmpty(requestParam.name()) ? parameter.getParameterName() : requestParam.name()); + String name = (requestParam == null || StringUtils.isEmpty(requestParam.name()) ? + parameter.getParameterName() : requestParam.name()); if (value == null) { builder.queryParam(name); @@ -305,12 +251,4 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethod } } - - private static class RequestPartResolver { - - public static Object resolvePart(HttpServletRequest servletRequest) throws Exception { - return servletRequest.getParts().toArray(new Part[servletRequest.getParts().size()]); - } - } - } diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java b/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java index 422517bd63d..905525b0392 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/MissingServletRequestPartException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2016 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. @@ -24,27 +24,28 @@ import org.springframework.web.multipart.MultipartResolver; * Raised when the part of a "multipart/form-data" request identified by its * name cannot be found. * - *

This may be because the request is not a multipart/form-data - * - * either because the part is not present in the request, or - * because the web application is not configured correctly for processing - * multipart requests -- e.g. no {@link MultipartResolver}. + *

This may be because the request is not a multipart/form-data request, + * because the part is not present in the request, or because the web + * application is not configured correctly for processing multipart requests, + * e.g. no {@link MultipartResolver}. * * @author Rossen Stoyanchev * @since 3.1 */ +@SuppressWarnings("serial") public class MissingServletRequestPartException extends ServletException { - private static final long serialVersionUID = -1255077391966870705L; - private final String partName; + public MissingServletRequestPartException(String partName) { - super("Required request part '" + partName + "' is not present."); + super("Required request part '" + partName + "' is not present"); this.partName = partName; } + public String getRequestPartName() { return this.partName; } + } diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartResolutionDelegate.java b/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartResolutionDelegate.java new file mode 100644 index 00000000000..6d6116f6b18 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/MultipartResolutionDelegate.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2016 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.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.Part; + +import org.springframework.core.GenericCollectionTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.util.ClassUtils; +import org.springframework.web.multipart.MultipartException; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.util.WebUtils; + +/** + * A common delegate for {@code HandlerMethodArgumentResolver} implementations + * which need to resolve {@link MultipartFile} and {@link Part} arguments. + * + * @author Juergen Hoeller + * @since 4.3 + */ +public abstract class MultipartResolutionDelegate { + + public static final Object UNRESOLVABLE = new Object(); + + + private static Class servletPartClass = null; + + static { + try { + servletPartClass = ClassUtils.forName( + "javax.servlet.http.Part", MultipartResolutionDelegate.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // Servlet 3.0 Part type not available - Part references simply not supported then. + } + } + + + public static boolean isMultipartRequest(HttpServletRequest request) { + return (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null || + isMultipartContent(request)); + } + + private static boolean isMultipartContent(HttpServletRequest request) { + String contentType = request.getContentType(); + return (contentType != null && contentType.toLowerCase().startsWith("multipart/")); + } + + static MultipartHttpServletRequest asMultipartHttpServletRequest(HttpServletRequest request) { + MultipartHttpServletRequest unwrapped = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); + if (unwrapped != null) { + return unwrapped; + } + return adaptToMultipartHttpServletRequest(request); + } + + private static MultipartHttpServletRequest adaptToMultipartHttpServletRequest(HttpServletRequest request) { + if (servletPartClass != null) { + // Servlet 3.0 available .. + return new StandardMultipartHttpServletRequest(request); + } + throw new MultipartException("Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); + } + + + public static boolean isMultipartArgument(MethodParameter parameter) { + Class paramType = parameter.getNestedParameterType(); + return (MultipartFile.class == paramType || isMultipartFileCollection(parameter) || + isMultipartFileArray(parameter) || servletPartClass == paramType || + isPartCollection(parameter) || isPartArray(parameter)); + } + + public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request) + throws Exception { + + MultipartHttpServletRequest multipartRequest = + WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); + boolean isMultipart = (multipartRequest != null || isMultipartContent(request)); + + if (MultipartFile.class == parameter.getNestedParameterType()) { + if (multipartRequest == null && isMultipart) { + multipartRequest = adaptToMultipartHttpServletRequest(request); + } + return (multipartRequest != null ? multipartRequest.getFile(name) : null); + } + else if (isMultipartFileCollection(parameter)) { + if (multipartRequest == null && isMultipart) { + multipartRequest = adaptToMultipartHttpServletRequest(request); + } + return (multipartRequest != null ? multipartRequest.getFiles(name) : null); + } + else if (isMultipartFileArray(parameter)) { + if (multipartRequest == null && isMultipart) { + multipartRequest = adaptToMultipartHttpServletRequest(request); + } + if (multipartRequest != null) { + List multipartFiles = multipartRequest.getFiles(name); + return multipartFiles.toArray(new MultipartFile[multipartFiles.size()]); + } + else { + return null; + } + } + else if (parameter.getNestedParameterType() == servletPartClass) { + return (isMultipart ? RequestPartResolver.resolvePart(request, name) : null); + } + else if (isPartCollection(parameter)) { + return (isMultipart ? RequestPartResolver.resolvePartList(request, name) : null); + } + else if (isPartArray(parameter)) { + return (isMultipart ? RequestPartResolver.resolvePartArray(request, name) : null); + } + else { + return UNRESOLVABLE; + } + } + + private static boolean isMultipartFileCollection(MethodParameter methodParam) { + return (MultipartFile.class == getCollectionParameterType(methodParam)); + } + + private static boolean isMultipartFileArray(MethodParameter methodParam) { + return (MultipartFile.class == methodParam.getNestedParameterType().getComponentType()); + } + + private static boolean isPartCollection(MethodParameter methodParam) { + return (servletPartClass == getCollectionParameterType(methodParam)); + } + + private static boolean isPartArray(MethodParameter methodParam) { + return (servletPartClass == methodParam.getNestedParameterType().getComponentType()); + } + + private static Class getCollectionParameterType(MethodParameter methodParam) { + Class paramType = methodParam.getNestedParameterType(); + if (Collection.class == paramType || List.class.isAssignableFrom(paramType)){ + Class valueType = GenericCollectionTypeResolver.getCollectionParameterType(methodParam); + if (valueType != null) { + return valueType; + } + } + return null; + } + + + /** + * Inner class to avoid hard-coded dependency on Servlet 3.0 Part type... + */ + private static class RequestPartResolver { + + public static Object resolvePart(HttpServletRequest servletRequest, String name) throws Exception { + return servletRequest.getPart(name); + } + + public static Object resolvePartList(HttpServletRequest servletRequest, String name) throws Exception { + Collection parts = servletRequest.getParts(); + List result = new ArrayList(parts.size()); + for (Part part : parts) { + if (part.getName().equals(name)) { + result.add(part); + } + } + return result; + } + + public static Object resolvePartArray(HttpServletRequest servletRequest, String name) throws Exception { + Collection parts = servletRequest.getParts(); + List result = new ArrayList(parts.size()); + for (Part part : parts) { + if (part.getName().equals(name)) { + result.add(part); + } + } + return result.toArray(new Part[result.size()]); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java index b26637e632f..b69e2fa2305 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/RequestPartServletServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -26,13 +26,10 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.util.ClassUtils; import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartResolver; -import org.springframework.web.util.WebUtils; - /** * {@link ServerHttpRequest} implementation that accesses one part of a multipart @@ -54,46 +51,26 @@ public class RequestPartServletServerHttpRequest extends ServletServerHttpReques /** - * Create a new instance. - * @param request the current request + * Create a new {@code RequestPartServletServerHttpRequest} instance. + * @param request the current servlet request * @param partName the name of the part to adapt to the {@link ServerHttpRequest} contract * @throws MissingServletRequestPartException if the request part cannot be found - * @throws IllegalArgumentException if MultipartHttpServletRequest cannot be initialized + * @throws MultipartException if MultipartHttpServletRequest cannot be initialized */ public RequestPartServletServerHttpRequest(HttpServletRequest request, String partName) throws MissingServletRequestPartException { super(request); - this.multipartRequest = asMultipartRequest(request); + this.multipartRequest = MultipartResolutionDelegate.asMultipartHttpServletRequest(request); this.partName = partName; this.headers = this.multipartRequest.getMultipartHeaders(this.partName); if (this.headers == null) { - if (request instanceof MultipartHttpServletRequest) { - throw new MissingServletRequestPartException(partName); - } - else { - throw new IllegalArgumentException( - "Failed to obtain request part: " + partName + ". " + - "The part is missing or multipart processing is not configured. " + - "Check for a MultipartResolver bean or if Servlet 3.0 multipart processing is enabled."); - } + throw new MissingServletRequestPartException(partName); } } - private static MultipartHttpServletRequest asMultipartRequest(HttpServletRequest request) { - MultipartHttpServletRequest unwrapped = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); - if (unwrapped != null) { - return unwrapped; - } - else if (ClassUtils.hasMethod(HttpServletRequest.class, "getParts")) { - // Servlet 3.0 available .. - return new StandardMultipartHttpServletRequest(request); - } - throw new IllegalArgumentException("Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - } - @Override public HttpHeaders getHeaders() { diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java index 4617715a8ca..6636c13945a 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -37,6 +37,7 @@ import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.mock.web.test.MockMultipartFile; import org.springframework.mock.web.test.MockMultipartHttpServletRequest; import org.springframework.mock.web.test.MockPart; +import org.springframework.util.ReflectionUtils; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.RequestParam; @@ -49,6 +50,7 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.support.MissingServletRequestPartException; import static org.junit.Assert.*; import static org.mockito.BDDMockito.*; @@ -82,22 +84,18 @@ public class RequestParamMethodArgumentResolverTests { private MethodParameter paramRequired; private MethodParameter paramNotRequired; private MethodParameter paramOptional; + private MethodParameter multipartFileOptional; private NativeWebRequest webRequest; private MockHttpServletRequest request; + @Before public void setUp() throws Exception { resolver = new RequestParamMethodArgumentResolver(null, true); - ParameterNameDiscoverer paramNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); - - Method method = getClass().getMethod("params", String.class, String[].class, - Map.class, MultipartFile.class, List.class, MultipartFile[].class, - Part.class, List.class, Part[].class, Map.class, - String.class, MultipartFile.class, List.class, Part.class, - MultipartFile.class, String.class, String.class, Optional.class); + Method method = ReflectionUtils.findMethod(getClass(), "handle", (Class[]) null); paramNamedDefaultValueString = new SynthesizingMethodParameter(method, 0); paramNamedStringArray = new SynthesizingMethodParameter(method, 1); @@ -121,27 +119,35 @@ public class RequestParamMethodArgumentResolverTests { paramRequired = new SynthesizingMethodParameter(method, 15); paramNotRequired = new SynthesizingMethodParameter(method, 16); paramOptional = new SynthesizingMethodParameter(method, 17); + multipartFileOptional = new SynthesizingMethodParameter(method, 18); request = new MockHttpServletRequest(); webRequest = new ServletWebRequest(request, new MockHttpServletResponse()); } + @Test public void supportsParameter() { resolver = new RequestParamMethodArgumentResolver(null, true); - assertTrue("String parameter not supported", resolver.supportsParameter(paramNamedDefaultValueString)); - assertTrue("String array parameter not supported", resolver.supportsParameter(paramNamedStringArray)); - assertTrue("Named map not parameter supported", resolver.supportsParameter(paramNamedMap)); - assertTrue("MultipartFile parameter not supported", resolver.supportsParameter(paramMultipartFile)); - assertTrue("List parameter not supported", resolver.supportsParameter(paramMultipartFileList)); - assertTrue("MultipartFile[] parameter not supported", resolver.supportsParameter(paramMultipartFileArray)); - assertTrue("Part parameter not supported", resolver.supportsParameter(paramPart)); - assertTrue("List parameter not supported", resolver.supportsParameter(paramPartList)); - assertTrue("Part[] parameter not supported", resolver.supportsParameter(paramPartArray)); - 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)); + assertTrue(resolver.supportsParameter(paramNamedDefaultValueString)); + assertTrue(resolver.supportsParameter(paramNamedStringArray)); + assertTrue(resolver.supportsParameter(paramNamedMap)); + assertTrue(resolver.supportsParameter(paramMultipartFile)); + assertTrue(resolver.supportsParameter(paramMultipartFileList)); + assertTrue(resolver.supportsParameter(paramMultipartFileArray)); + assertTrue(resolver.supportsParameter(paramPart)); + assertTrue(resolver.supportsParameter(paramPartList)); + assertTrue(resolver.supportsParameter(paramPartArray)); + assertFalse(resolver.supportsParameter(paramMap)); + assertTrue(resolver.supportsParameter(paramStringNotAnnot)); + assertTrue(resolver.supportsParameter(paramMultipartFileNotAnnot)); + assertTrue(resolver.supportsParameter(paramMultipartFileListNotAnnot)); + assertTrue(resolver.supportsParameter(paramPartNotAnnot)); + assertFalse(resolver.supportsParameter(paramRequestPartAnnot)); + assertTrue(resolver.supportsParameter(paramRequired)); + assertTrue(resolver.supportsParameter(paramNotRequired)); + assertTrue(resolver.supportsParameter(paramOptional)); + assertTrue(resolver.supportsParameter(multipartFileOptional)); resolver = new RequestParamMethodArgumentResolver(null, false); assertFalse(resolver.supportsParameter(paramStringNotAnnot)); @@ -154,18 +160,16 @@ public class RequestParamMethodArgumentResolverTests { request.addParameter("name", expected); Object result = resolver.resolveArgument(paramNamedDefaultValueString, null, webRequest, null); - assertTrue(result instanceof String); assertEquals("Invalid result", expected, result); } @Test public void resolveStringArray() throws Exception { - String[] expected = new String[]{"foo", "bar"}; + String[] expected = new String[] {"foo", "bar"}; request.addParameter("name", expected); Object result = resolver.resolveArgument(paramNamedStringArray, null, webRequest, null); - assertTrue(result instanceof String[]); assertArrayEquals("Invalid result", expected, (String[]) result); } @@ -178,7 +182,6 @@ public class RequestParamMethodArgumentResolverTests { webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramMultipartFile, null, webRequest, null); - assertTrue(result instanceof MultipartFile); assertEquals("Invalid result", expected, result); } @@ -190,10 +193,10 @@ public class RequestParamMethodArgumentResolverTests { MultipartFile expected2 = new MockMultipartFile("mfilelist", "Hello World 2".getBytes()); request.addFile(expected1); request.addFile(expected2); + request.addFile(new MockMultipartFile("other", "Hello World 3".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramMultipartFileList, null, webRequest, null); - assertTrue(result instanceof List); assertEquals(Arrays.asList(expected1, expected2), result); } @@ -205,12 +208,13 @@ public class RequestParamMethodArgumentResolverTests { MultipartFile expected2 = new MockMultipartFile("mfilearray", "Hello World 2".getBytes()); request.addFile(expected1); request.addFile(expected2); + request.addFile(new MockMultipartFile("other", "Hello World 3".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramMultipartFileArray, null, webRequest, null); - assertTrue(result instanceof MultipartFile[]); MultipartFile[] parts = (MultipartFile[]) result; + assertEquals(2, parts.length); assertEquals(parts[0], expected1); assertEquals(parts[1], expected2); } @@ -225,7 +229,6 @@ public class RequestParamMethodArgumentResolverTests { webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramPart, null, webRequest, null); - assertTrue(result instanceof Part); assertEquals("Invalid result", expected, result); } @@ -233,16 +236,16 @@ public class RequestParamMethodArgumentResolverTests { @Test public void resolvePartList() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); - MockPart expected1 = new MockPart("pfilelist", "Hello World 1".getBytes()); - MockPart expected2 = new MockPart("pfilelist", "Hello World 2".getBytes()); request.setMethod("POST"); request.setContentType("multipart/form-data"); + MockPart expected1 = new MockPart("pfilelist", "Hello World 1".getBytes()); + MockPart expected2 = new MockPart("pfilelist", "Hello World 2".getBytes()); request.addPart(expected1); request.addPart(expected2); + request.addPart(new MockPart("other", "Hello World 3".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramPartList, null, webRequest, null); - assertTrue(result instanceof List); assertEquals(Arrays.asList(expected1, expected2), result); } @@ -256,12 +259,13 @@ public class RequestParamMethodArgumentResolverTests { request.setContentType("multipart/form-data"); request.addPart(expected1); request.addPart(expected2); + request.addPart(new MockPart("other", "Hello World 3".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramPartArray, null, webRequest, null); - assertTrue(result instanceof Part[]); Part[] parts = (Part[]) result; + assertEquals(2, parts.length); assertEquals(parts[0], expected1); assertEquals(parts[1], expected2); } @@ -274,7 +278,6 @@ public class RequestParamMethodArgumentResolverTests { webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramMultipartFileNotAnnot, null, webRequest, null); - assertTrue(result instanceof MultipartFile); assertEquals("Invalid result", expected, result); } @@ -289,7 +292,6 @@ public class RequestParamMethodArgumentResolverTests { webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramMultipartFileListNotAnnot, null, webRequest, null); - assertTrue(result instanceof List); assertEquals(Arrays.asList(expected1, expected2), result); } @@ -300,9 +302,7 @@ public class RequestParamMethodArgumentResolverTests { fail("Expected exception: request is not a multipart request"); } - // SPR-9079 - - @Test + @Test // SPR-9079 public void isMultipartRequestHttpPut() throws Exception { MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); MultipartFile expected = new MockMultipartFile("multipartFileList", "Hello World".getBytes()); @@ -311,17 +311,23 @@ public class RequestParamMethodArgumentResolverTests { webRequest = new ServletWebRequest(request); Object actual = resolver.resolveArgument(paramMultipartFileListNotAnnot, null, webRequest, null); - assertTrue(actual instanceof List); assertEquals(expected, ((List) actual).get(0)); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = MultipartException.class) + public void noMultipartContent() throws Exception { + request.setMethod("POST"); + resolver.resolveArgument(paramMultipartFile, null, webRequest, null); + fail("Expected exception: no multipart content"); + } + + @Test(expected = MissingServletRequestPartException.class) public void missingMultipartFile() throws Exception { request.setMethod("POST"); request.setContentType("multipart/form-data"); resolver.resolveArgument(paramMultipartFile, null, webRequest, null); - fail("Expected exception: request is not MultiPartHttpServletRequest but param is MultipartFile"); + fail("Expected exception: no such part found"); } @Test @@ -334,7 +340,6 @@ public class RequestParamMethodArgumentResolverTests { webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramPartNotAnnot, null, webRequest, null); - assertTrue(result instanceof Part); assertEquals("Invalid result", expected, result); } @@ -342,7 +347,6 @@ public class RequestParamMethodArgumentResolverTests { @Test public void resolveDefaultValue() throws Exception { Object result = resolver.resolveArgument(paramNamedDefaultValueString, null, webRequest, null); - assertTrue(result instanceof String); assertEquals("Invalid result", "bar", result); } @@ -353,11 +357,8 @@ public class RequestParamMethodArgumentResolverTests { fail("Expected exception"); } - // SPR-10578 - - @Test + @Test // SPR-10578 public void missingRequestParamEmptyValueConvertedToNull() throws Exception { - WebDataBinder binder = new WebRequestDataBinder(null); binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); @@ -367,13 +368,11 @@ public class RequestParamMethodArgumentResolverTests { this.request.addParameter("stringNotAnnot", ""); Object arg = resolver.resolveArgument(paramStringNotAnnot, null, webRequest, binderFactory); - assertNull(arg); } @Test public void missingRequestParamEmptyValueNotRequired() throws Exception { - WebDataBinder binder = new WebRequestDataBinder(null); binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); @@ -383,7 +382,6 @@ public class RequestParamMethodArgumentResolverTests { this.request.addParameter("name", ""); Object arg = resolver.resolveArgument(paramNotRequired, null, webRequest, binderFactory); - assertNull(arg); } @@ -396,17 +394,13 @@ public class RequestParamMethodArgumentResolverTests { assertEquals("plainValue", result); } - // SPR-8561 - - @Test + @Test // SPR-8561 public void resolveSimpleTypeParamToNull() throws Exception { Object result = resolver.resolveArgument(paramStringNotAnnot, null, webRequest, null); assertNull(result); } - // SPR-10180 - - @Test + @Test // SPR-10180 public void resolveEmptyValueToDefault() throws Exception { this.request.addParameter("name", ""); Object result = resolver.resolveArgument(paramNamedDefaultValueString, null, webRequest, null); @@ -429,13 +423,12 @@ public class RequestParamMethodArgumentResolverTests { @Test @SuppressWarnings("rawtypes") - public void resolveOptional() throws Exception { + public void resolveOptionalParamValue() throws Exception { ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); initializer.setConversionService(new DefaultConversionService()); WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer); Object result = resolver.resolveArgument(paramOptional, null, webRequest, binderFactory); - assertEquals(Optional.class, result.getClass()); assertEquals(Optional.empty(), result); this.request.addParameter("name", "123"); @@ -444,8 +437,46 @@ public class RequestParamMethodArgumentResolverTests { assertEquals(123, ((Optional) result).get()); } + @Test + public void resolveOptionalMultipartFile() throws Exception { + ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); + initializer.setConversionService(new DefaultConversionService()); + WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer); - public void params(@RequestParam(name = "name", defaultValue = "bar") String param1, + MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); + MultipartFile expected = new MockMultipartFile("mfile", "Hello World".getBytes()); + request.addFile(expected); + webRequest = new ServletWebRequest(request); + + Object result = resolver.resolveArgument(multipartFileOptional, null, webRequest, binderFactory); + assertTrue(result instanceof Optional); + assertEquals("Invalid result", expected, ((Optional) result).get()); + } + + @Test + public void missingOptionalMultipartFile() throws Exception { + ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); + initializer.setConversionService(new DefaultConversionService()); + WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer); + + request.setMethod("POST"); + request.setContentType("multipart/form-data"); + assertEquals(Optional.empty(), resolver.resolveArgument(multipartFileOptional, null, webRequest, binderFactory)); + } + + @Test + public void optionalMultipartFileWithoutMultipartRequest() throws Exception { + ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); + initializer.setConversionService(new DefaultConversionService()); + WebDataBinderFactory binderFactory = new DefaultDataBinderFactory(initializer); + + assertEquals(Optional.empty(), resolver.resolveArgument(multipartFileOptional, null, webRequest, binderFactory)); + } + + + @SuppressWarnings("unused") + public void handle( + @RequestParam(name = "name", defaultValue = "bar") String param1, @RequestParam("name") String[] param2, @RequestParam("name") Map param3, @RequestParam("mfile") MultipartFile param4, @@ -462,7 +493,8 @@ public class RequestParamMethodArgumentResolverTests { @RequestPart MultipartFile requestPartAnnot, @RequestParam("name") String paramRequired, @RequestParam(name = "name", required = false) String paramNotRequired, - @RequestParam("name") Optional paramOptional) { + @RequestParam("name") Optional paramOptional, + @RequestParam("mfile") Optional multipartFileOptional) { } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java index 31f44a6e64a..1dd9c354a15 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -48,12 +48,13 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth super(null); } + @Override public boolean supportsParameter(MethodParameter parameter) { if (!parameter.hasParameterAnnotation(MatrixVariable.class)) { return false; } - if (Map.class.isAssignableFrom(parameter.getParameterType())) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { String variableName = parameter.getParameterAnnotation(MatrixVariable.class).name(); return StringUtils.hasText(variableName); } @@ -67,13 +68,10 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth } @Override + @SuppressWarnings("unchecked") protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { - - @SuppressWarnings("unchecked") - Map> pathParameters = - (Map>) request.getAttribute( - HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); - + Map> pathParameters = (Map>) + request.getAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); if (CollectionUtils.isEmpty(pathParameters)) { return null; } @@ -92,10 +90,10 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth for (MultiValueMap params : pathParameters.values()) { if (params.containsKey(name)) { if (found) { - String paramType = parameter.getParameterType().getName(); + String paramType = parameter.getNestedParameterType().getName(); throw new ServletRequestBindingException( "Found more than one match for URI path parameter '" + name + - "' for parameter type [" + paramType + "]. Use pathVar attribute to disambiguate."); + "' for parameter type [" + paramType + "]. Use 'pathVar' attribute to disambiguate."); } paramValues.addAll(params.get(name)); found = true; @@ -117,7 +115,7 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth @Override protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException { throw new ServletRequestBindingException("Missing matrix variable '" + name + - "' for method parameter of type " + parameter.getParameterType().getSimpleName()); + "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName()); } @@ -127,4 +125,5 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth super(annotation.name(), annotation.required(), annotation.defaultValue()); } } -} \ No newline at end of file + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java index ec3c7e24318..e2e30c447c4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java @@ -79,7 +79,7 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod if (!parameter.hasParameterAnnotation(PathVariable.class)) { return false; } - if (Map.class.isAssignableFrom(parameter.getParameterType())) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { String paramName = parameter.getParameterAnnotation(PathVariable.class).value(); return StringUtils.hasText(paramName); } @@ -95,10 +95,9 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod @Override @SuppressWarnings("unchecked") protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { - Map uriTemplateVars = - (Map) request.getAttribute( - HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); - return (uriTemplateVars != null) ? uriTemplateVars.get(name) : null; + Map uriTemplateVars = (Map) request.getAttribute( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + return (uriTemplateVars != null ? uriTemplateVars.get(name) : null); } @Override @@ -127,7 +126,7 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod public void contributeMethodArgument(MethodParameter parameter, Object value, UriComponentsBuilder builder, Map uriVariables, ConversionService conversionService) { - if (Map.class.isAssignableFrom(parameter.getParameterType())) { + if (Map.class.isAssignableFrom(parameter.getNestedParameterType())) { return; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java index 59d59d8f998..1c8af84a656 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -16,20 +16,15 @@ package org.springframework.web.servlet.mvc.method.annotation; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; - import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.Part; -import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.lang.UsesJava8; -import org.springframework.util.Assert; import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.WebDataBinder; @@ -41,12 +36,10 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.multipart.support.MultipartResolutionDelegate; import org.springframework.web.multipart.support.RequestPartServletServerHttpRequest; -import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; -import org.springframework.web.util.WebUtils; /** * Resolves the following method arguments: @@ -65,12 +58,13 @@ import org.springframework.web.util.WebUtils; * it is derived from the name of the method argument. * *

Automatic validation may be applied if the argument is annotated with - * {@code @javax.validation.Valid}. In case of validation failure, a - * {@link MethodArgumentNotValidException} is raised and a 400 response status - * code returned if {@link DefaultHandlerExceptionResolver} is configured. + * {@code @javax.validation.Valid}. In case of validation failure, a {@link MethodArgumentNotValidException} + * is raised and a 400 response status code returned if + * {@link org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver} is configured. * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Juergen Hoeller * @since 3.1 */ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver { @@ -107,18 +101,10 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM return true; } else { - if (parameter.hasParameterAnnotation(RequestParam.class)){ - return false; - } - else if (MultipartFile.class == parameter.getParameterType()) { - return true; - } - else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) { - return true; - } - else { + if (parameter.hasParameterAnnotation(RequestParam.class)) { return false; } + return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional()); } } @@ -128,87 +114,58 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM NativeWebRequest request, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); - assertIsMultipartRequest(servletRequest); + RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class); + boolean isRequired = ((requestPart == null || requestPart.required()) && !parameter.isOptional()); - MultipartHttpServletRequest multipartRequest = - WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class); + String name = getPartName(parameter, requestPart); + parameter = parameter.nestedIfOptional(); + Object arg = null; - Class paramType = parameter.getParameterType(); - boolean optional = paramType.getName().equals("java.util.Optional"); - if (optional) { - parameter.increaseNestingLevel(); - paramType = parameter.getNestedParameterType(); - } - - String partName = getPartName(parameter); - Object arg; - - if (MultipartFile.class == paramType) { - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - arg = multipartRequest.getFile(partName); - } - else if (isMultipartFileCollection(parameter)) { - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - arg = multipartRequest.getFiles(partName); - } - else if (isMultipartFileArray(parameter)) { - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - List files = multipartRequest.getFiles(partName); - arg = files.toArray(new MultipartFile[files.size()]); - } - else if ("javax.servlet.http.Part".equals(paramType.getName())) { - assertIsMultipartRequest(servletRequest); - arg = servletRequest.getPart(partName); - } - else if (isPartCollection(parameter)) { - assertIsMultipartRequest(servletRequest); - arg = new ArrayList(servletRequest.getParts()); - } - else if (isPartArray(parameter)) { - assertIsMultipartRequest(servletRequest); - arg = RequestPartResolver.resolvePart(servletRequest); + Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); + if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { + arg = mpArg; } else { try { - HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, partName); + HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, name); arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType()); - WebDataBinder binder = binderFactory.createBinder(request, arg, partName); + WebDataBinder binder = binderFactory.createBinder(request, arg, name); if (arg != null) { validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } - mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + partName, binder.getBindingResult()); + mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } catch (MissingServletRequestPartException ex) { - // handled below - arg = null; + if (isRequired) { + throw ex; + } + } + catch (MultipartException ex) { + if (isRequired) { + throw ex; + } } } - RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class); - boolean isRequired = ((requestPart == null || requestPart.required()) && !optional); - if (arg == null && isRequired) { - throw new MissingServletRequestPartException(partName); + if (!MultipartResolutionDelegate.isMultipartRequest(servletRequest)) { + throw new MultipartException("Current request is not a multipart request"); + } + else { + throw new MissingServletRequestPartException(name); + } } - if (optional) { - arg = Optional.ofNullable(arg); + if (parameter.isOptional()) { + arg = OptionalResolver.resolveValue(arg); } return arg; } - private static void assertIsMultipartRequest(HttpServletRequest request) { - String contentType = request.getContentType(); - if (contentType == null || !contentType.toLowerCase().startsWith("multipart/")) { - throw new MultipartException("The current request is not a multipart request"); - } - } - - private String getPartName(MethodParameter methodParam) { - RequestPart requestPart = methodParam.getParameterAnnotation(RequestPart.class); + private String getPartName(MethodParameter methodParam, RequestPart requestPart) { String partName = (requestPart != null ? requestPart.name() : ""); if (partName.length() == 0) { partName = methodParam.getParameterName(); @@ -221,46 +178,18 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM return partName; } - private boolean isMultipartFileCollection(MethodParameter methodParam) { - Class collectionType = getCollectionParameterType(methodParam); - return MultipartFile.class == collectionType; - } - - private boolean isMultipartFileArray(MethodParameter methodParam) { - Class paramType = methodParam.getNestedParameterType().getComponentType(); - return MultipartFile.class == paramType; - } - - private boolean isPartCollection(MethodParameter methodParam) { - Class collectionType = getCollectionParameterType(methodParam); - return (collectionType != null && "javax.servlet.http.Part".equals(collectionType.getName())); - } - - private boolean isPartArray(MethodParameter methodParam) { - Class paramType = methodParam.getNestedParameterType().getComponentType(); - return (paramType != null && "javax.servlet.http.Part".equals(paramType.getName())); - } - - private Class getCollectionParameterType(MethodParameter methodParam) { - Class paramType = methodParam.getNestedParameterType(); - if (Collection.class == paramType || List.class.isAssignableFrom(paramType)){ - Class valueType = GenericCollectionTypeResolver.getCollectionParameterType(methodParam); - if (valueType != null) { - return valueType; - } - } - return null; - } - /** - * Inner class to avoid hard-coded dependency on Servlet 3.0 Part type... + * Inner class to avoid hard-coded dependency on Java 8 Optional type... */ - private static class RequestPartResolver { + private static class OptionalResolver { - public static Object resolvePart(HttpServletRequest servletRequest) throws Exception { - Collection parts = servletRequest.getParts(); - return parts.toArray(new Part[parts.size()]); + public static Object resolveValue(Object value) { + if (value == null || (value instanceof Collection && ((Collection) value).isEmpty()) || + (value instanceof Object[] && ((Object[]) value).length == 0)) { + return Optional.empty(); + } + return Optional.of(value); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletCookieValueMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletCookieValueMethodArgumentResolver.java index 8c51d5d8d2a..cf174ea1346 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletCookieValueMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletCookieValueMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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. @@ -52,7 +52,7 @@ public class ServletCookieValueMethodArgumentResolver extends AbstractCookieValu protected Object resolveName(String cookieName, MethodParameter parameter, NativeWebRequest webRequest) throws Exception { HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); Cookie cookieValue = WebUtils.getCookie(servletRequest, cookieName); - if (Cookie.class.isAssignableFrom(parameter.getParameterType())) { + if (Cookie.class.isAssignableFrom(parameter.getNestedParameterType())) { return cookieValue; } else if (cookieValue != null) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java index 8044d66412a..3eb84cbe6a9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java @@ -262,6 +262,12 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(0); } + public ConcurrentResultMethodParameter(ConcurrentResultMethodParameter original) { + super(original); + this.returnValue = original.returnValue; + this.returnType = original.returnType; + } + @Override public Class getParameterType() { if (this.returnValue != null) { @@ -283,6 +289,11 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { public Type getGenericParameterType() { return this.returnType.getType(); } + + @Override + public ConcurrentResultMethodParameter clone() { + return new ConcurrentResultMethodParameter(this); + } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java index 5b443ff7fdb..716acda541a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -41,6 +41,7 @@ import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.mock.web.test.MockMultipartFile; import org.springframework.mock.web.test.MockMultipartHttpServletRequest; import org.springframework.mock.web.test.MockPart; +import org.springframework.util.ReflectionUtils; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.WebDataBinder; @@ -53,7 +54,6 @@ import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.MissingServletRequestPartException; -import org.springframework.web.multipart.support.RequestPartServletServerHttpRequest; import static org.junit.Assert.*; import static org.mockito.BDDMockito.*; @@ -86,7 +86,9 @@ public class RequestPartMethodArgumentResolverTests { private MethodParameter paramPartArray; private MethodParameter paramRequestParamAnnot; private MethodParameter optionalMultipartFile; + private MethodParameter optionalMultipartFileList; private MethodParameter optionalPart; + private MethodParameter optionalPartList; private MethodParameter optionalRequestPart; private NativeWebRequest webRequest; @@ -97,10 +99,7 @@ public class RequestPartMethodArgumentResolverTests { @SuppressWarnings("unchecked") @Before public void setUp() throws Exception { - Method method = getClass().getMethod("handle", SimpleBean.class, SimpleBean.class, - SimpleBean.class, MultipartFile.class, List.class, MultipartFile[].class, - Integer.TYPE, MultipartFile.class, Part.class, List.class, Part[].class, - MultipartFile.class, Optional.class, Optional.class, Optional.class); + Method method = ReflectionUtils.findMethod(getClass(), "handle", (Class[]) null); paramRequestPart = new SynthesizingMethodParameter(method, 0); paramRequestPart.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); @@ -119,9 +118,13 @@ public class RequestPartMethodArgumentResolverTests { paramRequestParamAnnot = new SynthesizingMethodParameter(method, 11); optionalMultipartFile = new SynthesizingMethodParameter(method, 12); optionalMultipartFile.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); - optionalPart = new SynthesizingMethodParameter(method, 13); + optionalMultipartFileList = new SynthesizingMethodParameter(method, 13); + optionalMultipartFileList.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + optionalPart = new SynthesizingMethodParameter(method, 14); optionalPart.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); - optionalRequestPart = new SynthesizingMethodParameter(method, 14); + optionalPartList = new SynthesizingMethodParameter(method, 15); + optionalPartList.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + optionalRequestPart = new SynthesizingMethodParameter(method, 16); messageConverter = mock(HttpMessageConverter.class); given(messageConverter.getSupportedMediaTypes()).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN)); @@ -136,35 +139,41 @@ public class RequestPartMethodArgumentResolverTests { multipartRequest = new MockMultipartHttpServletRequest(); multipartRequest.addFile(multipartFile1); multipartRequest.addFile(multipartFile2); + multipartRequest.addFile(new MockMultipartFile("otherPart", "", "text/plain", content)); webRequest = new ServletWebRequest(multipartRequest, new MockHttpServletResponse()); } @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(paramPart)); - assertTrue("List parameter not supported", resolver.supportsParameter(paramPartList)); - assertTrue("Part[] parameter not supported", resolver.supportsParameter(paramPartArray)); - assertTrue("MultipartFile parameter not supported", resolver.supportsParameter(paramMultipartFile)); - assertTrue("List parameter not supported", resolver.supportsParameter(paramMultipartFileList)); - assertTrue("MultipartFile[] parameter not supported", resolver.supportsParameter(paramMultipartFileArray)); - assertFalse("non-RequestPart parameter should not be supported", resolver.supportsParameter(paramInt)); - assertFalse("@RequestParam args should not be supported", resolver.supportsParameter(paramRequestParamAnnot)); + assertTrue(resolver.supportsParameter(paramRequestPart)); + assertTrue(resolver.supportsParameter(paramNamedRequestPart)); + assertTrue(resolver.supportsParameter(paramValidRequestPart)); + assertTrue(resolver.supportsParameter(paramMultipartFile)); + assertTrue(resolver.supportsParameter(paramMultipartFileList)); + assertTrue(resolver.supportsParameter(paramMultipartFileArray)); + assertFalse(resolver.supportsParameter(paramInt)); + assertTrue(resolver.supportsParameter(paramMultipartFileNotAnnot)); + assertTrue(resolver.supportsParameter(paramPart)); + assertTrue(resolver.supportsParameter(paramPartList)); + assertTrue(resolver.supportsParameter(paramPartArray)); + assertFalse(resolver.supportsParameter(paramRequestParamAnnot)); + assertTrue(resolver.supportsParameter(optionalMultipartFile)); + assertTrue(resolver.supportsParameter(optionalMultipartFileList)); + assertTrue(resolver.supportsParameter(optionalPart)); + assertTrue(resolver.supportsParameter(optionalPartList)); + assertTrue(resolver.supportsParameter(optionalRequestPart)); } @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); } @@ -175,6 +184,7 @@ public class RequestPartMethodArgumentResolverTests { assertNotNull(actual); assertTrue(actual instanceof MultipartFile[]); MultipartFile[] parts = (MultipartFile[]) actual; + assertEquals(2, parts.length); assertEquals(parts[0], multipartFile1); assertEquals(parts[1], multipartFile2); } @@ -184,6 +194,7 @@ public class RequestPartMethodArgumentResolverTests { MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); MultipartFile expected = new MockMultipartFile("multipartFileNotAnnot", "Hello World".getBytes()); request.addFile(expected); + request.addFile(new MockMultipartFile("otherPart", "", "text/plain", "Hello World".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramMultipartFileNotAnnot, null, webRequest, null); @@ -194,52 +205,52 @@ public class RequestPartMethodArgumentResolverTests { @Test public void resolvePartArgument() throws Exception { - MockPart expected = new MockPart("part", "Hello World".getBytes()); MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("POST"); request.setContentType("multipart/form-data"); + MockPart expected = new MockPart("part", "Hello World".getBytes()); request.addPart(expected); + request.addPart(new MockPart("otherPart", "Hello World".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramPart, null, webRequest, null); - assertTrue(result instanceof Part); assertEquals("Invalid result", expected, result); } @Test public void resolvePartListArgument() throws Exception { - MockPart part1 = new MockPart("requestPart1", "Hello World 1".getBytes()); - MockPart part2 = new MockPart("requestPart2", "Hello World 2".getBytes()); MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("POST"); request.setContentType("multipart/form-data"); + MockPart part1 = new MockPart("requestPart", "Hello World 1".getBytes()); + MockPart part2 = new MockPart("requestPart", "Hello World 2".getBytes()); request.addPart(part1); request.addPart(part2); + request.addPart(new MockPart("otherPart", "Hello World".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramPartList, null, webRequest, null); - assertTrue(result instanceof List); assertEquals(Arrays.asList(part1, part2), result); } @Test public void resolvePartArrayArgument() throws Exception { - MockPart part1 = new MockPart("requestPart1", "Hello World 1".getBytes()); - MockPart part2 = new MockPart("requestPart2", "Hello World 2".getBytes()); MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("POST"); request.setContentType("multipart/form-data"); + MockPart part1 = new MockPart("requestPart", "Hello World 1".getBytes()); + MockPart part2 = new MockPart("requestPart", "Hello World 2".getBytes()); request.addPart(part1); request.addPart(part2); + request.addPart(new MockPart("otherPart", "Hello World".getBytes())); webRequest = new ServletWebRequest(request); Object result = resolver.resolveArgument(paramPartArray, null, webRequest, null); - assertTrue(result instanceof Part[]); Part[] parts = (Part[]) result; - assertThat(parts, Matchers.arrayWithSize(2)); + assertEquals(2, parts.length); assertEquals(parts[0], part1); assertEquals(parts[1], part2); } @@ -302,8 +313,8 @@ public class RequestPartMethodArgumentResolverTests { @Test // SPR-9079 public void isMultipartRequestPut() throws Exception { this.multipartRequest.setMethod("PUT"); - Object actual = resolver.resolveArgument(paramMultipartFile, null, webRequest, null); - assertSame(multipartFile1, actual); + Object actualValue = resolver.resolveArgument(paramMultipartFile, null, webRequest, null); + assertSame(multipartFile1, actualValue); } @Test @@ -311,12 +322,16 @@ public class RequestPartMethodArgumentResolverTests { MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); MultipartFile expected = new MockMultipartFile("optionalMultipartFile", "Hello World".getBytes()); request.addFile(expected); + request.addFile(new MockMultipartFile("otherPart", "", "text/plain", "Hello World".getBytes())); webRequest = new ServletWebRequest(request); - Object result = resolver.resolveArgument(optionalMultipartFile, null, webRequest, null); + Object actualValue = resolver.resolveArgument(optionalMultipartFile, null, webRequest, null); + assertTrue(actualValue instanceof Optional); + assertEquals("Invalid result", expected, ((Optional) actualValue).get()); - assertTrue(result instanceof Optional); - assertEquals("Invalid result", expected, ((Optional) result).get()); + actualValue = resolver.resolveArgument(optionalMultipartFile, null, webRequest, null); + assertTrue(actualValue instanceof Optional); + assertEquals("Invalid result", expected, ((Optional) actualValue).get()); } @Test @@ -324,10 +339,62 @@ public class RequestPartMethodArgumentResolverTests { MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); webRequest = new ServletWebRequest(request); - Object result = resolver.resolveArgument(optionalMultipartFile, null, webRequest, null); + Object actualValue = resolver.resolveArgument(optionalMultipartFile, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); - assertTrue(result instanceof Optional); - assertFalse("Invalid result", ((Optional) result).isPresent()); + actualValue = resolver.resolveArgument(optionalMultipartFile, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + } + + @Test + public void resolveOptionalMultipartFileArgumentWithoutMultipartRequest() throws Exception { + webRequest = new ServletWebRequest(new MockHttpServletRequest()); + + Object actualValue = resolver.resolveArgument(optionalMultipartFile, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + + actualValue = resolver.resolveArgument(optionalMultipartFile, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + } + + @Test + public void resolveOptionalMultipartFileList() throws Exception { + MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); + MultipartFile expected = new MockMultipartFile("requestPart", "Hello World".getBytes()); + request.addFile(expected); + request.addFile(new MockMultipartFile("otherPart", "", "text/plain", "Hello World".getBytes())); + webRequest = new ServletWebRequest(request); + + Object actualValue = resolver.resolveArgument(optionalMultipartFileList, null, webRequest, null); + assertTrue(actualValue instanceof Optional); + assertEquals("Invalid result", Collections.singletonList(expected), ((Optional) actualValue).get()); + + actualValue = resolver.resolveArgument(optionalMultipartFileList, null, webRequest, null); + assertTrue(actualValue instanceof Optional); + assertEquals("Invalid result", Collections.singletonList(expected), ((Optional) actualValue).get()); + } + + @Test + public void resolveOptionalMultipartFileListNotPresent() throws Exception { + MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); + webRequest = new ServletWebRequest(request); + + Object actualValue = resolver.resolveArgument(optionalMultipartFileList, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + + actualValue = resolver.resolveArgument(optionalMultipartFileList, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + } + + @Test + public void resolveOptionalMultipartFileListWithoutMultipartRequest() throws Exception { + webRequest = new ServletWebRequest(new MockHttpServletRequest()); + + Object actualValue = resolver.resolveArgument(optionalMultipartFileList, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + + actualValue = resolver.resolveArgument(optionalMultipartFileList, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); } @Test @@ -337,12 +404,16 @@ public class RequestPartMethodArgumentResolverTests { request.setMethod("POST"); request.setContentType("multipart/form-data"); request.addPart(expected); + request.addPart(new MockPart("otherPart", "Hello World".getBytes())); webRequest = new ServletWebRequest(request); - Object result = resolver.resolveArgument(optionalPart, null, webRequest, null); + Object actualValue = resolver.resolveArgument(optionalPart, null, webRequest, null); + assertTrue(actualValue instanceof Optional); + assertEquals("Invalid result", expected, ((Optional) actualValue).get()); - assertTrue(result instanceof Optional); - assertEquals("Invalid result", expected, ((Optional) result).get()); + actualValue = resolver.resolveArgument(optionalPart, null, webRequest, null); + assertTrue(actualValue instanceof Optional); + assertEquals("Invalid result", expected, ((Optional) actualValue).get()); } @Test @@ -352,36 +423,108 @@ public class RequestPartMethodArgumentResolverTests { request.setContentType("multipart/form-data"); webRequest = new ServletWebRequest(request); - Object result = resolver.resolveArgument(optionalPart, null, webRequest, null); + Object actualValue = resolver.resolveArgument(optionalPart, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); - assertTrue(result instanceof Optional); - assertFalse("Invalid result", ((Optional) result).isPresent()); + actualValue = resolver.resolveArgument(optionalPart, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + } + + @Test + public void resolveOptionalPartArgumentWithoutMultipartRequest() throws Exception { + webRequest = new ServletWebRequest(new MockHttpServletRequest()); + + Object actualValue = resolver.resolveArgument(optionalPart, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + + actualValue = resolver.resolveArgument(optionalPart, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + } + + @Test + public void resolveOptionalPartList() throws Exception { + MockPart expected = new MockPart("requestPart", "Hello World".getBytes()); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setContentType("multipart/form-data"); + request.addPart(expected); + request.addPart(new MockPart("otherPart", "Hello World".getBytes())); + webRequest = new ServletWebRequest(request); + + Object actualValue = resolver.resolveArgument(optionalPartList, null, webRequest, null); + assertTrue(actualValue instanceof Optional); + assertEquals("Invalid result", Collections.singletonList(expected), ((Optional) actualValue).get()); + + actualValue = resolver.resolveArgument(optionalPartList, null, webRequest, null); + assertTrue(actualValue instanceof Optional); + assertEquals("Invalid result", Collections.singletonList(expected), ((Optional) actualValue).get()); + } + + @Test + public void resolveOptionalPartListNotPresent() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setContentType("multipart/form-data"); + webRequest = new ServletWebRequest(request); + + Object actualValue = resolver.resolveArgument(optionalPartList, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + + actualValue = resolver.resolveArgument(optionalPartList, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + } + + @Test + public void resolveOptionalPartListWithoutMultipartRequest() throws Exception { + webRequest = new ServletWebRequest(new MockHttpServletRequest()); + + Object actualValue = resolver.resolveArgument(optionalPartList, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + + actualValue = resolver.resolveArgument(optionalPartList, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); } @Test public void resolveOptionalRequestPart() throws Exception { SimpleBean simpleBean = new SimpleBean("foo"); - given(messageConverter.canRead(SimpleBean.class, MediaType.TEXT_PLAIN)).willReturn(true); given(messageConverter.read(eq(SimpleBean.class), isA(HttpInputMessage.class))).willReturn(simpleBean); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - Object actualValue = resolver.resolveArgument(optionalRequestPart, mavContainer, webRequest, new ValidatingBinderFactory()); + Object actualValue = resolver.resolveArgument(optionalRequestPart, mavContainer, webRequest, new ValidatingBinderFactory()); + assertEquals("Invalid argument value", Optional.of(simpleBean), actualValue); + assertFalse("The requestHandled flag shouldn't change", mavContainer.isRequestHandled()); + + actualValue = resolver.resolveArgument(optionalRequestPart, mavContainer, webRequest, new ValidatingBinderFactory()); assertEquals("Invalid argument value", Optional.of(simpleBean), actualValue); assertFalse("The requestHandled flag shouldn't change", mavContainer.isRequestHandled()); } @Test public void resolveOptionalRequestPartNotPresent() throws Exception { - given(messageConverter.canRead(SimpleBean.class, MediaType.TEXT_PLAIN)).willReturn(true); - given(messageConverter.read(eq(SimpleBean.class), isA(RequestPartServletServerHttpRequest.class))).willReturn(null); - - ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - Object actualValue = resolver.resolveArgument(optionalRequestPart, mavContainer, webRequest, new ValidatingBinderFactory()); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setContentType("multipart/form-data"); + webRequest = new ServletWebRequest(request); + Object actualValue = resolver.resolveArgument(optionalRequestPart, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + + actualValue = resolver.resolveArgument(optionalRequestPart, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + } + + @Test + public void resolveOptionalRequestPartWithoutMultipartRequest() throws Exception { + webRequest = new ServletWebRequest(new MockHttpServletRequest()); + + Object actualValue = resolver.resolveArgument(optionalRequestPart, null, webRequest, null); + assertEquals("Invalid argument value", Optional.empty(), actualValue); + + actualValue = resolver.resolveArgument(optionalRequestPart, null, webRequest, null); assertEquals("Invalid argument value", Optional.empty(), actualValue); - assertFalse("The requestHandled flag shouldn't change", mavContainer.isRequestHandled()); } @@ -390,8 +533,8 @@ public class RequestPartMethodArgumentResolverTests { given(messageConverter.read(eq(SimpleBean.class), isA(HttpInputMessage.class))).willReturn(argValue); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - Object actualValue = resolver.resolveArgument(parameter, mavContainer, webRequest, new ValidatingBinderFactory()); + Object actualValue = resolver.resolveArgument(parameter, mavContainer, webRequest, new ValidatingBinderFactory()); assertEquals("Invalid argument value", argValue, actualValue); assertFalse("The requestHandled flag shouldn't change", mavContainer.isRequestHandled()); } @@ -425,22 +568,26 @@ public class RequestPartMethodArgumentResolverTests { } } + @SuppressWarnings("unused") - 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, - @RequestPart("requestPart") MultipartFile[] multipartFileArray, - int i, - MultipartFile multipartFileNotAnnot, - Part part, - @RequestPart("part") List partList, - @RequestPart("part") Part[] partArray, - @RequestParam MultipartFile requestParamAnnot, - Optional optionalMultipartFile, - Optional optionalPart, - @RequestPart("requestPart") Optional optionalRequestPart) { + 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, + @RequestPart("requestPart") MultipartFile[] multipartFileArray, + int i, + MultipartFile multipartFileNotAnnot, + Part part, + @RequestPart("requestPart") List partList, + @RequestPart("requestPart") Part[] partArray, + @RequestParam MultipartFile requestParamAnnot, + Optional optionalMultipartFile, + @RequestPart("requestPart") Optional> optionalMultipartFileList, + Optional optionalPart, + @RequestPart("requestPart") Optional> optionalPartList, + @RequestPart("requestPart") Optional optionalRequestPart) { } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java index f7c35c2aee2..21bf3834bf1 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 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. @@ -47,7 +47,9 @@ import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMeth import static org.junit.Assert.*; -/** @author Arjen Poutsma */ +/** + * @author Arjen Poutsma + */ public class DefaultHandlerExceptionResolverTests { private DefaultHandlerExceptionResolver exceptionResolver; @@ -174,7 +176,9 @@ public class DefaultHandlerExceptionResolverTests { assertNotNull("No ModelAndView returned", mav); assertTrue("No Empty ModelAndView returned", mav.isEmpty()); assertEquals("Invalid status code", 400, response.getStatus()); - assertEquals("Required request part 'name' is not present.", response.getErrorMessage()); + assertTrue(response.getErrorMessage().contains("request part")); + assertTrue(response.getErrorMessage().contains("name")); + assertTrue(response.getErrorMessage().contains("not present")); } @Test @@ -211,6 +215,7 @@ public class DefaultHandlerExceptionResolverTests { assertSame(ex, request.getAttribute("javax.servlet.error.exception")); } + @SuppressWarnings("unused") public void handle(String arg) { }