Skip to content

Commit d61c0ee

Browse files
committed
Consistent support for path variable and multipart binding
Closes gh-24107 Closes gh-22169 Closes gh-25265
1 parent 5bdbbdf commit d61c0ee

File tree

13 files changed

+365
-85
lines changed

13 files changed

+365
-85
lines changed

spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,11 +17,14 @@
1717
package org.springframework.web.bind;
1818

1919
import javax.servlet.ServletRequest;
20+
import javax.servlet.http.HttpServletRequest;
2021

2122
import org.springframework.beans.MutablePropertyValues;
2223
import org.springframework.lang.Nullable;
24+
import org.springframework.util.StringUtils;
2325
import org.springframework.validation.BindException;
2426
import org.springframework.web.multipart.MultipartRequest;
27+
import org.springframework.web.multipart.support.StandardServletPartUtils;
2528
import org.springframework.web.util.WebUtils;
2629

2730
/**
@@ -103,6 +106,12 @@ public void bind(ServletRequest request) {
103106
if (multipartRequest != null) {
104107
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
105108
}
109+
else if (StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")) {
110+
HttpServletRequest httpServletRequest = WebUtils.getNativeRequest(request, HttpServletRequest.class);
111+
if (httpServletRequest != null) {
112+
StandardServletPartUtils.bindParts(httpServletRequest, mpvs, isBindEmptyMultipartFiles());
113+
}
114+
}
106115
addBindValues(mpvs, request);
107116
doBind(mpvs);
108117
}

spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@
3737
* binding from URL query params or form data in the request data to Java objects.
3838
*
3939
* @author Rossen Stoyanchev
40+
* @author Juergen Hoeller
4041
* @since 5.0
4142
*/
4243
public class WebExchangeDataBinder extends WebDataBinder {
@@ -64,7 +65,7 @@ public WebExchangeDataBinder(@Nullable Object target, String objectName) {
6465

6566
/**
6667
* Bind query params, form data, and or multipart form data to the binder target.
67-
* @param exchange the current exchange.
68+
* @param exchange the current exchange
6869
* @return a {@code Mono<Void>} when binding is complete
6970
*/
7071
public Mono<Void> bind(ServerWebExchange exchange) {
@@ -76,8 +77,11 @@ public Mono<Void> bind(ServerWebExchange exchange) {
7677
/**
7778
* Protected method to obtain the values for data binding. By default this
7879
* method delegates to {@link #extractValuesToBind(ServerWebExchange)}.
80+
* @param exchange the current exchange
81+
* @return a map of bind values
82+
* @since 5.3
7983
*/
80-
protected Mono<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) {
84+
public Mono<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) {
8185
return extractValuesToBind(exchange);
8286
}
8387

@@ -107,7 +111,7 @@ public static Mono<Map<String, Object>> extractValuesToBind(ServerWebExchange ex
107111
});
108112
}
109113

110-
private static void addBindValue(Map<String, Object> params, String key, List<?> values) {
114+
protected static void addBindValue(Map<String, Object> params, String key, List<?> values) {
111115
if (!CollectionUtils.isEmpty(values)) {
112116
values = values.stream()
113117
.map(value -> value instanceof FormFieldPart ? ((FormFieldPart) value).value() : value)

spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,19 +17,16 @@
1717
package org.springframework.web.bind.support;
1818

1919
import javax.servlet.http.HttpServletRequest;
20-
import javax.servlet.http.Part;
2120

2221
import org.springframework.beans.MutablePropertyValues;
2322
import org.springframework.lang.Nullable;
24-
import org.springframework.util.LinkedMultiValueMap;
25-
import org.springframework.util.MultiValueMap;
2623
import org.springframework.util.StringUtils;
2724
import org.springframework.validation.BindException;
2825
import org.springframework.web.bind.WebDataBinder;
2926
import org.springframework.web.context.request.NativeWebRequest;
3027
import org.springframework.web.context.request.WebRequest;
31-
import org.springframework.web.multipart.MultipartException;
3228
import org.springframework.web.multipart.MultipartRequest;
29+
import org.springframework.web.multipart.support.StandardServletPartUtils;
3330

3431
/**
3532
* Special {@link org.springframework.validation.DataBinder} to perform data binding
@@ -109,53 +106,21 @@ public WebRequestDataBinder(@Nullable Object target, String objectName) {
109106
*/
110107
public void bind(WebRequest request) {
111108
MutablePropertyValues mpvs = new MutablePropertyValues(request.getParameterMap());
112-
if (isMultipartRequest(request) && request instanceof NativeWebRequest) {
109+
if (request instanceof NativeWebRequest) {
113110
MultipartRequest multipartRequest = ((NativeWebRequest) request).getNativeRequest(MultipartRequest.class);
114111
if (multipartRequest != null) {
115112
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
116113
}
117-
else {
114+
else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "multipart/")) {
118115
HttpServletRequest servletRequest = ((NativeWebRequest) request).getNativeRequest(HttpServletRequest.class);
119116
if (servletRequest != null) {
120-
bindParts(servletRequest, mpvs);
117+
StandardServletPartUtils.bindParts(servletRequest, mpvs, isBindEmptyMultipartFiles());
121118
}
122119
}
123120
}
124121
doBind(mpvs);
125122
}
126123

127-
/**
128-
* Check if the request is a multipart request (by checking its Content-Type header).
129-
* @param request the request with parameters to bind
130-
*/
131-
private boolean isMultipartRequest(WebRequest request) {
132-
String contentType = request.getHeader("Content-Type");
133-
return StringUtils.startsWithIgnoreCase(contentType, "multipart");
134-
}
135-
136-
private void bindParts(HttpServletRequest request, MutablePropertyValues mpvs) {
137-
try {
138-
MultiValueMap<String, Part> map = new LinkedMultiValueMap<>();
139-
for (Part part : request.getParts()) {
140-
map.add(part.getName(), part);
141-
}
142-
map.forEach((key, values) -> {
143-
if (values.size() == 1) {
144-
Part part = values.get(0);
145-
if (isBindEmptyMultipartFiles() || part.getSize() > 0) {
146-
mpvs.add(key, part);
147-
}
148-
}
149-
else {
150-
mpvs.add(key, values);
151-
}
152-
});
153-
}
154-
catch (Exception ex) {
155-
throw new MultipartException("Failed to get request parts", ex);
156-
}
157-
}
158-
159124
/**
160125
* Treats errors as fatal.
161126
* <p>Use this method only if it's an error if the input isn't valid.

spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,9 @@
2828
import java.util.Optional;
2929
import java.util.Set;
3030

31+
import javax.servlet.http.HttpServletRequest;
32+
import javax.servlet.http.Part;
33+
3134
import org.apache.commons.logging.Log;
3235
import org.apache.commons.logging.LogFactory;
3336

@@ -39,6 +42,7 @@
3942
import org.springframework.core.annotation.AnnotationUtils;
4043
import org.springframework.lang.Nullable;
4144
import org.springframework.util.Assert;
45+
import org.springframework.util.StringUtils;
4246
import org.springframework.validation.BindException;
4347
import org.springframework.validation.BindingResult;
4448
import org.springframework.validation.Errors;
@@ -53,6 +57,9 @@
5357
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
5458
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
5559
import org.springframework.web.method.support.ModelAndViewContainer;
60+
import org.springframework.web.multipart.MultipartFile;
61+
import org.springframework.web.multipart.MultipartRequest;
62+
import org.springframework.web.multipart.support.StandardServletPartUtils;
5663

5764
/**
5865
* Resolve {@code @ModelAttribute} annotated method arguments and handle
@@ -242,15 +249,9 @@ protected Object createAttribute(String attributeName, MethodParameter parameter
242249
* @throws Exception in case of constructor invocation failure
243250
* @since 5.1
244251
*/
245-
@SuppressWarnings("deprecation")
246252
protected Object constructAttribute(Constructor<?> ctor, String attributeName, MethodParameter parameter,
247253
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
248254

249-
Object constructed = constructAttribute(ctor, attributeName, binderFactory, webRequest);
250-
if (constructed != null) {
251-
return constructed;
252-
}
253-
254255
if (ctor.getParameterCount() == 0) {
255256
// A single default constructor -> clearly a standard JavaBeans arrangement.
256257
return BeanUtils.instantiateClass(ctor);
@@ -279,10 +280,13 @@ protected Object constructAttribute(Constructor<?> ctor, String attributeName, M
279280
if (fieldDefaultPrefix != null) {
280281
value = webRequest.getParameter(fieldDefaultPrefix + paramName);
281282
}
282-
if (value == null && fieldMarkerPrefix != null) {
283-
if (webRequest.getParameter(fieldMarkerPrefix + paramName) != null) {
283+
if (value == null) {
284+
if (fieldMarkerPrefix != null && webRequest.getParameter(fieldMarkerPrefix + paramName) != null) {
284285
value = binder.getEmptyValue(paramType);
285286
}
287+
else {
288+
value = resolveConstructorArgument(paramName, paramType, webRequest);
289+
}
286290
}
287291
}
288292
try {
@@ -320,20 +324,6 @@ protected Object constructAttribute(Constructor<?> ctor, String attributeName, M
320324
return BeanUtils.instantiateClass(ctor, args);
321325
}
322326

323-
/**
324-
* Construct a new attribute instance with the given constructor.
325-
* @since 5.0
326-
* @deprecated as of 5.1, in favor of
327-
* {@link #constructAttribute(Constructor, String, MethodParameter, WebDataBinderFactory, NativeWebRequest)}
328-
*/
329-
@Deprecated
330-
@Nullable
331-
protected Object constructAttribute(Constructor<?> ctor, String attributeName,
332-
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
333-
334-
return null;
335-
}
336-
337327
/**
338328
* Extension point to bind the request to the target object.
339329
* @param binder the data binder instance to use for the binding
@@ -343,6 +333,29 @@ protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest requ
343333
((WebRequestDataBinder) binder).bind(request);
344334
}
345335

336+
@Nullable
337+
public Object resolveConstructorArgument(String paramName, Class<?> paramType, NativeWebRequest request)
338+
throws Exception {
339+
340+
MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
341+
if (multipartRequest != null) {
342+
List<MultipartFile> files = multipartRequest.getFiles(paramName);
343+
if (!files.isEmpty()) {
344+
return (files.size() == 1 ? files.get(0) : files);
345+
}
346+
}
347+
else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "multipart/")) {
348+
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
349+
if (servletRequest != null) {
350+
List<Part> parts = StandardServletPartUtils.getParts(servletRequest, paramName);
351+
if (!parts.isEmpty()) {
352+
return (parts.size() == 1 ? parts.get(0) : parts);
353+
}
354+
}
355+
}
356+
return null;
357+
}
358+
346359
/**
347360
* Validate the model attribute if applicable.
348361
* <p>The default implementation checks for {@code @javax.validation.Valid},
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.multipart.support;
18+
19+
import java.util.LinkedList;
20+
import java.util.List;
21+
22+
import javax.servlet.http.HttpServletRequest;
23+
import javax.servlet.http.Part;
24+
25+
import org.springframework.beans.MutablePropertyValues;
26+
import org.springframework.util.LinkedMultiValueMap;
27+
import org.springframework.util.MultiValueMap;
28+
import org.springframework.web.multipart.MultipartException;
29+
30+
/**
31+
* Utility methods for standard Servlet {@link Part} handling.
32+
*
33+
* @author Juergen Hoeller
34+
* @since 5.3
35+
* @see HttpServletRequest#getParts()
36+
* @see StandardServletMultipartResolver
37+
*/
38+
public abstract class StandardServletPartUtils {
39+
40+
/**
41+
* Retrieve all parts from the given servlet request.
42+
* @param request the servlet request
43+
* @return the parts in a MultiValueMap
44+
* @throws MultipartException in case of failures
45+
*/
46+
public static MultiValueMap<String, Part> getParts(HttpServletRequest request) throws MultipartException {
47+
try {
48+
MultiValueMap<String, Part> parts = new LinkedMultiValueMap<>();
49+
for (Part part : request.getParts()) {
50+
parts.add(part.getName(), part);
51+
}
52+
return parts;
53+
}
54+
catch (Exception ex) {
55+
throw new MultipartException("Failed to get request parts", ex);
56+
}
57+
}
58+
59+
/**
60+
* Retrieve all parts with the given name from the given servlet request.
61+
* @param request the servlet request
62+
* @param name the name to look for
63+
* @return the parts in a MultiValueMap
64+
* @throws MultipartException in case of failures
65+
*/
66+
public static List<Part> getParts(HttpServletRequest request, String name) throws MultipartException {
67+
try {
68+
List<Part> parts = new LinkedList<>();
69+
for (Part part : request.getParts()) {
70+
if (part.getName().equals(name)) {
71+
parts.add(part);
72+
}
73+
}
74+
return parts;
75+
}
76+
catch (Exception ex) {
77+
throw new MultipartException("Failed to get request parts", ex);
78+
}
79+
}
80+
81+
/**
82+
* Bind all parts from the given servlet request.
83+
* @param request the servlet request
84+
* @param mpvs the property values to bind to
85+
* @param bindEmpty whether to bind empty parts as well
86+
* @throws MultipartException in case of failures
87+
*/
88+
public static void bindParts(HttpServletRequest request, MutablePropertyValues mpvs, boolean bindEmpty)
89+
throws MultipartException {
90+
91+
getParts(request).forEach((key, values) -> {
92+
if (values.size() == 1) {
93+
Part part = values.get(0);
94+
if (bindEmpty || part.getSize() > 0) {
95+
mpvs.add(key, part);
96+
}
97+
}
98+
else {
99+
mpvs.add(key, values);
100+
}
101+
});
102+
}
103+
104+
}

0 commit comments

Comments
 (0)