diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java index 8a4dbdad65b..357ce9098cc 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 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,11 +17,14 @@ package org.springframework.web.bind; import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; import org.springframework.beans.MutablePropertyValues; import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.web.multipart.MultipartRequest; +import org.springframework.web.multipart.support.StandardServletPartUtils; import org.springframework.web.util.WebUtils; /** @@ -103,6 +106,12 @@ public class ServletRequestDataBinder extends WebDataBinder { if (multipartRequest != null) { bindMultipart(multipartRequest.getMultiFileMap(), mpvs); } + else if (StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")) { + HttpServletRequest httpServletRequest = WebUtils.getNativeRequest(request, HttpServletRequest.class); + if (httpServletRequest != null) { + StandardServletPartUtils.bindParts(httpServletRequest, mpvs, isBindEmptyMultipartFiles()); + } + } addBindValues(mpvs, request); doBind(mpvs); } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java index 4cb29e13f04..99b6627b5e2 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 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.web.server.ServerWebExchange; * binding from URL query params or form data in the request data to Java objects. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 5.0 */ public class WebExchangeDataBinder extends WebDataBinder { @@ -64,7 +65,7 @@ public class WebExchangeDataBinder extends WebDataBinder { /** * Bind query params, form data, and or multipart form data to the binder target. - * @param exchange the current exchange. + * @param exchange the current exchange * @return a {@code Mono} when binding is complete */ public Mono bind(ServerWebExchange exchange) { @@ -76,8 +77,11 @@ public class WebExchangeDataBinder extends WebDataBinder { /** * Protected method to obtain the values for data binding. By default this * method delegates to {@link #extractValuesToBind(ServerWebExchange)}. + * @param exchange the current exchange + * @return a map of bind values + * @since 5.3 */ - protected Mono> getValuesToBind(ServerWebExchange exchange) { + public Mono> getValuesToBind(ServerWebExchange exchange) { return extractValuesToBind(exchange); } @@ -107,7 +111,7 @@ public class WebExchangeDataBinder extends WebDataBinder { }); } - private static void addBindValue(Map params, String key, List values) { + protected static void addBindValue(Map params, String key, List values) { if (!CollectionUtils.isEmpty(values)) { values = values.stream() .map(value -> value instanceof FormFieldPart ? ((FormFieldPart) value).value() : value) diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java index eff3cde7703..7b6a1fd9324 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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,19 +17,16 @@ package org.springframework.web.bind.support; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.Part; import org.springframework.beans.MutablePropertyValues; import org.springframework.lang.Nullable; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.WebRequest; -import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartRequest; +import org.springframework.web.multipart.support.StandardServletPartUtils; /** * Special {@link org.springframework.validation.DataBinder} to perform data binding @@ -109,53 +106,21 @@ public class WebRequestDataBinder extends WebDataBinder { */ public void bind(WebRequest request) { MutablePropertyValues mpvs = new MutablePropertyValues(request.getParameterMap()); - if (isMultipartRequest(request) && request instanceof NativeWebRequest) { + if (request instanceof NativeWebRequest) { MultipartRequest multipartRequest = ((NativeWebRequest) request).getNativeRequest(MultipartRequest.class); if (multipartRequest != null) { bindMultipart(multipartRequest.getMultiFileMap(), mpvs); } - else { + else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "multipart/")) { HttpServletRequest servletRequest = ((NativeWebRequest) request).getNativeRequest(HttpServletRequest.class); if (servletRequest != null) { - bindParts(servletRequest, mpvs); + StandardServletPartUtils.bindParts(servletRequest, mpvs, isBindEmptyMultipartFiles()); } } } doBind(mpvs); } - /** - * Check if the request is a multipart request (by checking its Content-Type header). - * @param request the request with parameters to bind - */ - private boolean isMultipartRequest(WebRequest request) { - String contentType = request.getHeader("Content-Type"); - return StringUtils.startsWithIgnoreCase(contentType, "multipart"); - } - - private void bindParts(HttpServletRequest request, MutablePropertyValues mpvs) { - try { - MultiValueMap map = new LinkedMultiValueMap<>(); - for (Part part : request.getParts()) { - map.add(part.getName(), part); - } - map.forEach((key, values) -> { - if (values.size() == 1) { - Part part = values.get(0); - if (isBindEmptyMultipartFiles() || part.getSize() > 0) { - mpvs.add(key, part); - } - } - else { - mpvs.add(key, values); - } - }); - } - catch (Exception ex) { - throw new MultipartException("Failed to get request parts", ex); - } - } - /** * Treats errors as fatal. *

Use this method only if it's an error if the input isn't valid. diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java index 340a039df6e..38e6f981cb2 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -28,6 +28,9 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.Part; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -39,6 +42,7 @@ import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -53,6 +57,9 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartRequest; +import org.springframework.web.multipart.support.StandardServletPartUtils; /** * Resolve {@code @ModelAttribute} annotated method arguments and handle @@ -242,15 +249,9 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol * @throws Exception in case of constructor invocation failure * @since 5.1 */ - @SuppressWarnings("deprecation") protected Object constructAttribute(Constructor ctor, String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception { - Object constructed = constructAttribute(ctor, attributeName, binderFactory, webRequest); - if (constructed != null) { - return constructed; - } - if (ctor.getParameterCount() == 0) { // A single default constructor -> clearly a standard JavaBeans arrangement. return BeanUtils.instantiateClass(ctor); @@ -279,10 +280,13 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol if (fieldDefaultPrefix != null) { value = webRequest.getParameter(fieldDefaultPrefix + paramName); } - if (value == null && fieldMarkerPrefix != null) { - if (webRequest.getParameter(fieldMarkerPrefix + paramName) != null) { + if (value == null) { + if (fieldMarkerPrefix != null && webRequest.getParameter(fieldMarkerPrefix + paramName) != null) { value = binder.getEmptyValue(paramType); } + else { + value = resolveConstructorArgument(paramName, paramType, webRequest); + } } } try { @@ -320,20 +324,6 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol return BeanUtils.instantiateClass(ctor, args); } - /** - * Construct a new attribute instance with the given constructor. - * @since 5.0 - * @deprecated as of 5.1, in favor of - * {@link #constructAttribute(Constructor, String, MethodParameter, WebDataBinderFactory, NativeWebRequest)} - */ - @Deprecated - @Nullable - protected Object constructAttribute(Constructor ctor, String attributeName, - WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception { - - return null; - } - /** * Extension point to bind the request to the target object. * @param binder the data binder instance to use for the binding @@ -343,6 +333,29 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol ((WebRequestDataBinder) binder).bind(request); } + @Nullable + public Object resolveConstructorArgument(String paramName, Class paramType, NativeWebRequest request) + throws Exception { + + MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class); + if (multipartRequest != null) { + List files = multipartRequest.getFiles(paramName); + if (!files.isEmpty()) { + return (files.size() == 1 ? files.get(0) : files); + } + } + else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "multipart/")) { + HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); + if (servletRequest != null) { + List parts = StandardServletPartUtils.getParts(servletRequest, paramName); + if (!parts.isEmpty()) { + return (parts.size() == 1 ? parts.get(0) : parts); + } + } + } + return null; + } + /** * Validate the model attribute if applicable. *

The default implementation checks for {@code @javax.validation.Valid}, diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/StandardServletPartUtils.java b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardServletPartUtils.java new file mode 100644 index 00000000000..b0e7c5e26fe --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/StandardServletPartUtils.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.LinkedList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.Part; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.multipart.MultipartException; + +/** + * Utility methods for standard Servlet {@link Part} handling. + * + * @author Juergen Hoeller + * @since 5.3 + * @see HttpServletRequest#getParts() + * @see StandardServletMultipartResolver + */ +public abstract class StandardServletPartUtils { + + /** + * Retrieve all parts from the given servlet request. + * @param request the servlet request + * @return the parts in a MultiValueMap + * @throws MultipartException in case of failures + */ + public static MultiValueMap getParts(HttpServletRequest request) throws MultipartException { + try { + MultiValueMap parts = new LinkedMultiValueMap<>(); + for (Part part : request.getParts()) { + parts.add(part.getName(), part); + } + return parts; + } + catch (Exception ex) { + throw new MultipartException("Failed to get request parts", ex); + } + } + + /** + * Retrieve all parts with the given name from the given servlet request. + * @param request the servlet request + * @param name the name to look for + * @return the parts in a MultiValueMap + * @throws MultipartException in case of failures + */ + public static List getParts(HttpServletRequest request, String name) throws MultipartException { + try { + List parts = new LinkedList<>(); + for (Part part : request.getParts()) { + if (part.getName().equals(name)) { + parts.add(part); + } + } + return parts; + } + catch (Exception ex) { + throw new MultipartException("Failed to get request parts", ex); + } + } + + /** + * Bind all parts from the given servlet request. + * @param request the servlet request + * @param mpvs the property values to bind to + * @param bindEmpty whether to bind empty parts as well + * @throws MultipartException in case of failures + */ + public static void bindParts(HttpServletRequest request, MutablePropertyValues mpvs, boolean bindEmpty) + throws MultipartException { + + getParts(request).forEach((key, values) -> { + if (values.size() == 1) { + Part part = values.get(0); + if (bindEmpty || part.getSize() > 0) { + mpvs.add(key, part); + } + } + else { + mpvs.add(key, values); + } + }); + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java b/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java index cb0849ae81b..d10616e6b64 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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,8 +16,16 @@ package org.springframework.web.reactive; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +import reactor.core.publisher.Mono; + +import org.springframework.http.codec.multipart.Part; import org.springframework.lang.Nullable; import org.springframework.ui.Model; +import org.springframework.util.MultiValueMap; import org.springframework.validation.support.BindingAwareConcurrentModel; import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -35,6 +43,7 @@ import org.springframework.web.server.ServerWebExchange; *

Container for the default model for the request. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 5.0 */ public class BindingContext { @@ -79,7 +88,7 @@ public class BindingContext { * @throws ServerErrorException if {@code @InitBinder} method invocation fails */ public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, @Nullable Object target, String name) { - WebExchangeDataBinder dataBinder = new WebExchangeDataBinder(target, name); + WebExchangeDataBinder dataBinder = new ExtendedWebExchangeDataBinder(target, name); if (this.initializer != null) { this.initializer.initBinder(dataBinder); } @@ -106,4 +115,34 @@ public class BindingContext { return createDataBinder(exchange, null, name); } + + /** + * Extended variant of {@link WebExchangeDataBinder}, adding path variables. + */ + private static class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder { + + public ExtendedWebExchangeDataBinder(@Nullable Object target, String objectName) { + super(target, objectName); + } + + @Override + public Mono> getValuesToBind(ServerWebExchange exchange) { + Map vars = exchange.getAttributeOrDefault( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Collections.emptyMap()); + MultiValueMap queryParams = exchange.getRequest().getQueryParams(); + Mono> formData = exchange.getFormData(); + Mono> multipartData = exchange.getMultipartData(); + + return Mono.zip(Mono.just(vars), Mono.just(queryParams), formData, multipartData) + .map(tuple -> { + Map result = new TreeMap<>(); + tuple.getT1().forEach(result::put); + tuple.getT2().forEach((key, values) -> addBindValue(result, key, values)); + tuple.getT3().forEach((key, values) -> addBindValue(result, key, values)); + tuple.getT4().forEach((key, values) -> addBindValue(result, key, values)); + return result; + }); + } + } + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java index c5ce3a36838..05a3fda827b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java @@ -41,7 +41,6 @@ import org.springframework.util.ClassUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeDataBinder; @@ -233,7 +232,8 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR } // A single data class constructor -> resolve constructor arguments from request parameters. - return WebExchangeDataBinder.extractValuesToBind(exchange).map(bindValues -> { + WebExchangeDataBinder binder = context.createDataBinder(exchange, null, attributeName); + return getValuesToBind(binder, exchange).map(bindValues -> { ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class); String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor)); Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor); @@ -241,7 +241,6 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR Assert.state(paramNames.length == paramTypes.length, () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor); Object[] args = new Object[paramTypes.length]; - WebDataBinder binder = context.createDataBinder(exchange, null, attributeName); String fieldDefaultPrefix = binder.getFieldDefaultPrefix(); String fieldMarkerPrefix = binder.getFieldMarkerPrefix(); for (int i = 0; i < paramNames.length; i++) { @@ -271,6 +270,18 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR }); } + /** + * Protected method to obtain the values for data binding. By default this + * method delegates to {@link WebExchangeDataBinder#getValuesToBind}. + * @param binder the data binder in use + * @param exchange the current exchange + * @return a map of bind values + * @since 5.3 + */ + public Mono> getValuesToBind(WebExchangeDataBinder binder, ServerWebExchange exchange) { + return binder.getValuesToBind(exchange); + } + private boolean hasErrorsArgument(MethodParameter parameter) { int i = parameter.getParameterIndex(); Class[] paramTypes = parameter.getExecutable().getParameterTypes(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index 5f7c4d6fbf7..15b504430db 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -48,7 +48,6 @@ import org.springframework.web.method.ControllerAdviceBean; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; import org.springframework.web.method.annotation.MapMethodProcessor; -import org.springframework.web.method.annotation.ModelAttributeMethodProcessor; import org.springframework.web.method.annotation.ModelMethodProcessor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite; @@ -361,7 +360,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice)); // Annotation-based return value types - handlers.add(new ModelAttributeMethodProcessor(false)); + handlers.add(new ServletModelAttributeMethodProcessor(false)); handlers.add(new RequestResponseBodyMethodProcessor( getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice)); @@ -375,7 +374,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce } // Catch-all - handlers.add(new ModelAttributeMethodProcessor(true)); + handlers.add(new ServletModelAttributeMethodProcessor(true)); return handlers; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java index 3ee0c1f8131..a0db1cac45a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 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. @@ -31,6 +31,8 @@ import org.springframework.web.servlet.HandlerMapping; * * @author Rossen Stoyanchev * @since 3.1 + * @see ServletRequestDataBinder + * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE */ public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder { @@ -60,9 +62,9 @@ public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder { * Merge URI variables into the property values to use for data binding. */ @Override - @SuppressWarnings("unchecked") protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + @SuppressWarnings("unchecked") Map uriVars = (Map) request.getAttribute(attr); if (uriVars != null) { uriVars.forEach((name, value) -> { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewResolverMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewResolverMethodReturnValueHandler.java index 7b1e9ea5916..423a4b15131 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewResolverMethodReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewResolverMethodReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 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. @@ -58,7 +58,8 @@ public class ModelAndViewResolverMethodReturnValueHandler implements HandlerMeth @Nullable private final List mavResolvers; - private final ModelAttributeMethodProcessor modelAttributeProcessor = new ModelAttributeMethodProcessor(true); + private final ModelAttributeMethodProcessor modelAttributeProcessor = + new ServletModelAttributeMethodProcessor(true); /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 7dd9fde2590..64f77997872 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -75,7 +75,6 @@ import org.springframework.web.method.annotation.ErrorsMethodArgumentResolver; import org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver; import org.springframework.web.method.annotation.InitBinderDataBinderFactory; import org.springframework.web.method.annotation.MapMethodProcessor; -import org.springframework.web.method.annotation.ModelAttributeMethodProcessor; import org.springframework.web.method.annotation.ModelFactory; import org.springframework.web.method.annotation.ModelMethodProcessor; import org.springframework.web.method.annotation.RequestHeaderMapMethodArgumentResolver; @@ -739,7 +738,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory)); // Annotation-based return value types - handlers.add(new ModelAttributeMethodProcessor(false)); + handlers.add(new ServletModelAttributeMethodProcessor(false)); handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice)); @@ -757,7 +756,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers())); } else { - handlers.add(new ModelAttributeMethodProcessor(true)); + handlers.add(new ServletModelAttributeMethodProcessor(true)); } return handlers; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletModelAttributeMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletModelAttributeMethodProcessor.java index 1499c87c457..38cf420e141 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletModelAttributeMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletModelAttributeMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2020 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. @@ -108,8 +108,8 @@ public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodPr return null; } - @SuppressWarnings("unchecked") protected final Map getUriTemplateVariables(NativeWebRequest request) { + @SuppressWarnings("unchecked") Map variables = (Map) request.getAttribute( HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); return (variables != null ? variables : Collections.emptyMap()); @@ -158,4 +158,23 @@ public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodPr servletBinder.bind(servletRequest); } + @Override + @Nullable + public Object resolveConstructorArgument(String paramName, Class paramType, NativeWebRequest request) + throws Exception { + + Object value = super.resolveConstructorArgument(paramName, paramType, request); + if (value != null) { + return value; + } + ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); + if (servletRequest != null) { + String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + @SuppressWarnings("unchecked") + Map uriVars = (Map) servletRequest.getAttribute(attr); + return uriVars.get(paramName); + } + return null; + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index 3a61ec81291..6f4a618fb09 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -57,6 +57,7 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import javax.servlet.http.Part; import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; @@ -108,6 +109,7 @@ import org.springframework.ui.Model; import org.springframework.ui.ModelMap; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; +import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; @@ -139,6 +141,7 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.StringMultipartFileEditor; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.ModelAndView; @@ -158,6 +161,7 @@ import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import org.springframework.web.testfixture.servlet.MockMultipartFile; import org.springframework.web.testfixture.servlet.MockMultipartHttpServletRequest; +import org.springframework.web.testfixture.servlet.MockPart; import org.springframework.web.testfixture.servlet.MockServletConfig; import org.springframework.web.testfixture.servlet.MockServletContext; @@ -1934,6 +1938,44 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertThat(response.getContentAsString()).isEqualTo("value1-true-0"); } + @PathPatternsParameterizedTest + void dataClassBindingWithPathVariable(boolean usePathPatterns) throws Exception { + initDispatcherServlet(PathVariableDataClassController.class, usePathPatterns); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind/true"); + request.addParameter("param1", "value1"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertThat(response.getContentAsString()).isEqualTo("value1-true-0"); + } + + @PathPatternsParameterizedTest + void dataClassBindingWithMultipartFile(boolean usePathPatterns) throws Exception { + initDispatcherServlet(MultipartFileDataClassController.class, usePathPatterns); + + MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(); + request.setRequestURI("/bind"); + request.addFile(new MockMultipartFile("param1", "value1".getBytes(StandardCharsets.UTF_8))); + request.addParameter("param2", "true"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertThat(response.getContentAsString()).isEqualTo("value1-true-0"); + } + + @PathPatternsParameterizedTest + void dataClassBindingWithServletPart(boolean usePathPatterns) throws Exception { + initDispatcherServlet(ServletPartDataClassController.class, usePathPatterns); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setContentType("multipart/form-data"); + request.setRequestURI("/bind"); + request.addPart(new MockPart("param1", "value1".getBytes(StandardCharsets.UTF_8))); + request.addParameter("param2", "true"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertThat(response.getContentAsString()).isEqualTo("value1-true-0"); + } + @PathPatternsParameterizedTest void dataClassBindingWithAdditionalSetter(boolean usePathPatterns) throws Exception { initDispatcherServlet(DataClassController.class, usePathPatterns); @@ -3831,6 +3873,15 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl } } + @RestController + public static class PathVariableDataClassController { + + @RequestMapping("/bind/{param2}") + public String handle(DataClass data) { + return data.param1 + "-" + data.param2 + "-" + data.param3; + } + } + @RestController public static class ValidatedDataClassController { @@ -3873,6 +3924,70 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl } } + public static class MultipartFileDataClass { + + @NotNull + public final MultipartFile param1; + + public final boolean param2; + + public int param3; + + @ConstructorProperties({"param1", "param2", "optionalParam"}) + public MultipartFileDataClass(MultipartFile param1, boolean p2, Optional optionalParam) { + this.param1 = param1; + this.param2 = p2; + Assert.notNull(optionalParam, "Optional must not be null"); + optionalParam.ifPresent(integer -> this.param3 = integer); + } + + public void setParam3(int param3) { + this.param3 = param3; + } + } + + @RestController + public static class MultipartFileDataClassController { + + @RequestMapping("/bind") + public String handle(MultipartFileDataClass data) throws IOException { + return StreamUtils.copyToString(data.param1.getInputStream(), StandardCharsets.UTF_8) + + "-" + data.param2 + "-" + data.param3; + } + } + + public static class ServletPartDataClass { + + @NotNull + public final Part param1; + + public final boolean param2; + + public int param3; + + @ConstructorProperties({"param1", "param2", "optionalParam"}) + public ServletPartDataClass(Part param1, boolean p2, Optional optionalParam) { + this.param1 = param1; + this.param2 = p2; + Assert.notNull(optionalParam, "Optional must not be null"); + optionalParam.ifPresent(integer -> this.param3 = integer); + } + + public void setParam3(int param3) { + this.param3 = param3; + } + } + + @RestController + public static class ServletPartDataClassController { + + @RequestMapping("/bind") + public String handle(ServletPartDataClass data) throws IOException { + return StreamUtils.copyToString(data.param1.getInputStream(), StandardCharsets.UTF_8) + + "-" + data.param2 + "-" + data.param3; + } + } + @RestController public static class OptionalDataClassController {