Consistent support for path variable and multipart binding
Closes gh-24107 Closes gh-22169 Closes gh-25265
This commit is contained in:
		
							parent
							
								
									5bdbbdfcfb
								
							
						
					
					
						commit
						d61c0ee57d
					
				|  | @ -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); | ||||
| 	} | ||||
|  |  | |||
|  | @ -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<Void>} when binding is complete | ||||
| 	 */ | ||||
| 	public Mono<Void> 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<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) { | ||||
| 	public Mono<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) { | ||||
| 		return extractValuesToBind(exchange); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -107,7 +111,7 @@ public class WebExchangeDataBinder extends WebDataBinder { | |||
| 				}); | ||||
| 	} | ||||
| 
 | ||||
| 	private static void addBindValue(Map<String, Object> params, String key, List<?> values) { | ||||
| 	protected static void addBindValue(Map<String, Object> params, String key, List<?> values) { | ||||
| 		if (!CollectionUtils.isEmpty(values)) { | ||||
| 			values = values.stream() | ||||
| 					.map(value -> value instanceof FormFieldPart ? ((FormFieldPart) value).value() : value) | ||||
|  |  | |||
|  | @ -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<String, Part> 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. | ||||
| 	 * <p>Use this method only if it's an error if the input isn't valid. | ||||
|  |  | |||
|  | @ -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<MultipartFile> 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<Part> parts = StandardServletPartUtils.getParts(servletRequest, paramName); | ||||
| 				if (!parts.isEmpty()) { | ||||
| 					return (parts.size() == 1 ? parts.get(0) : parts); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Validate the model attribute if applicable. | ||||
| 	 * <p>The default implementation checks for {@code @javax.validation.Valid}, | ||||
|  |  | |||
|  | @ -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<String, Part> getParts(HttpServletRequest request) throws MultipartException { | ||||
| 		try { | ||||
| 			MultiValueMap<String, Part> 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<Part> getParts(HttpServletRequest request, String name) throws MultipartException { | ||||
| 		try { | ||||
| 			List<Part> 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); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  | @ -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; | |||
|  * <p>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<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) { | ||||
| 			Map<String, String> vars = exchange.getAttributeOrDefault( | ||||
| 					HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Collections.emptyMap()); | ||||
| 			MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams(); | ||||
| 			Mono<MultiValueMap<String, String>> formData = exchange.getFormData(); | ||||
| 			Mono<MultiValueMap<String, Part>> multipartData = exchange.getMultipartData(); | ||||
| 
 | ||||
| 			return Mono.zip(Mono.just(vars), Mono.just(queryParams), formData, multipartData) | ||||
| 					.map(tuple -> { | ||||
| 						Map<String, Object> 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; | ||||
| 					}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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<Map<String, Object>> getValuesToBind(WebExchangeDataBinder binder, ServerWebExchange exchange) { | ||||
| 		return binder.getValuesToBind(exchange); | ||||
| 	} | ||||
| 
 | ||||
| 	private boolean hasErrorsArgument(MethodParameter parameter) { | ||||
| 		int i = parameter.getParameterIndex(); | ||||
| 		Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes(); | ||||
|  |  | |||
|  | @ -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; | ||||
| 	} | ||||
|  |  | |||
|  | @ -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<String, String> uriVars = (Map<String, String>) request.getAttribute(attr); | ||||
| 		if (uriVars != null) { | ||||
| 			uriVars.forEach((name, value) -> { | ||||
|  |  | |||
|  | @ -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<ModelAndViewResolver> mavResolvers; | ||||
| 
 | ||||
| 	private final ModelAttributeMethodProcessor modelAttributeProcessor = new ModelAttributeMethodProcessor(true); | ||||
| 	private final ModelAttributeMethodProcessor modelAttributeProcessor = | ||||
| 			new ServletModelAttributeMethodProcessor(true); | ||||
| 
 | ||||
| 
 | ||||
| 	/** | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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<String, String> getUriTemplateVariables(NativeWebRequest request) { | ||||
| 		@SuppressWarnings("unchecked") | ||||
| 		Map<String, String> variables = (Map<String, String>) 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<String, String> uriVars = (Map<String, String>) servletRequest.getAttribute(attr); | ||||
| 			return uriVars.get(paramName); | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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<Integer> 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<Integer> 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 { | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue