Skip to content

Commit b28b3e8

Browse files
committed
Add WebExchangeDataBinder
Issue: SPR-14541
1 parent 580b8b9 commit b28b3e8

File tree

9 files changed

+454
-10
lines changed

9 files changed

+454
-10
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,8 @@ project("spring-web-reactive") {
815815
optional "org.apache.httpcomponents:httpclient:${httpclientVersion}"
816816
optional('org.webjars:webjars-locator:0.32')
817817
testCompile("javax.validation:validation-api:${beanvalVersion}")
818+
testCompile("org.hibernate:hibernate-validator:${hibval5Version}")
819+
testCompile("javax.el:javax.el-api:${elApiVersion}")
818820
testCompile("org.apache.tomcat:tomcat-util:${tomcatVersion}")
819821
testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}")
820822
testCompile("org.eclipse.jetty:jetty-server:${jettyVersion}")

spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@
5555
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
5656
import org.springframework.util.ClassUtils;
5757
import org.springframework.validation.Errors;
58+
import org.springframework.validation.MessageCodesResolver;
5859
import org.springframework.validation.Validator;
60+
import org.springframework.web.bind.WebDataBinder;
61+
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
5962
import org.springframework.web.cors.CorsConfiguration;
6063
import org.springframework.web.reactive.HandlerMapping;
6164
import org.springframework.web.reactive.accept.CompositeContentTypeResolver;
@@ -255,6 +258,7 @@ public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
255258
}
256259

257260
adapter.setMessageReaders(getMessageReaders());
261+
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());
258262
adapter.setConversionService(mvcConversionService());
259263
adapter.setValidator(mvcValidator());
260264

@@ -325,6 +329,18 @@ protected final void addDefaultHttpMessageReaders(List<HttpMessageReader<?>> rea
325329
protected void extendMessageReaders(List<HttpMessageReader<?>> messageReaders) {
326330
}
327331

332+
/**
333+
* Return the {@link ConfigurableWebBindingInitializer} to use for
334+
* initializing all {@link WebDataBinder} instances.
335+
*/
336+
protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() {
337+
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
338+
initializer.setConversionService(mvcConversionService());
339+
initializer.setValidator(mvcValidator());
340+
initializer.setMessageCodesResolver(getMessageCodesResolver());
341+
return initializer;
342+
}
343+
328344
@Bean
329345
public FormattingConversionService mvcConversionService() {
330346
FormattingConversionService service = new DefaultFormattingConversionService();
@@ -378,6 +394,13 @@ protected Validator getValidator() {
378394
return null;
379395
}
380396

397+
/**
398+
* Override this method to provide a custom {@link MessageCodesResolver}.
399+
*/
400+
protected MessageCodesResolver getMessageCodesResolver() {
401+
return null;
402+
}
403+
381404
@Bean
382405
public SimpleHandlerAdapter simpleHandlerAdapter() {
383406
return new SimpleHandlerAdapter();

spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.ui.ExtendedModelMap;
4242
import org.springframework.ui.ModelMap;
4343
import org.springframework.validation.Validator;
44+
import org.springframework.web.bind.support.WebBindingInitializer;
4445
import org.springframework.web.method.HandlerMethod;
4546
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
4647
import org.springframework.web.reactive.HandlerAdapter;
@@ -68,6 +69,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
6869

6970
private ReactiveAdapterRegistry reactiveAdapters = new ReactiveAdapterRegistry();
7071

72+
private WebBindingInitializer webBindingInitializer;
73+
7174
private ConversionService conversionService = new DefaultFormattingConversionService();
7275

7376
private Validator validator;
@@ -136,6 +139,21 @@ public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
136139
return this.reactiveAdapters;
137140
}
138141

142+
/**
143+
* Provide a WebBindingInitializer with "global" initialization to apply
144+
* to every DataBinder instance.
145+
*/
146+
public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) {
147+
this.webBindingInitializer = webBindingInitializer;
148+
}
149+
150+
/**
151+
* Return the configured WebBindingInitializer, or {@code null} if none.
152+
*/
153+
public WebBindingInitializer getWebBindingInitializer() {
154+
return this.webBindingInitializer;
155+
}
156+
139157
/**
140158
* Configure a ConversionService for type conversion of controller method
141159
* arguments as well as for converting from different async types to

spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
import org.springframework.util.MimeTypeUtils;
5050
import org.springframework.validation.Validator;
5151
import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean;
52+
import org.springframework.web.bind.WebExchangeDataBinder;
53+
import org.springframework.web.bind.support.WebBindingInitializer;
5254
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
5355
import org.springframework.web.reactive.handler.AbstractHandlerMapping;
5456
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
@@ -161,6 +163,13 @@ public void requestMappingHandlerAdapter() throws Exception {
161163
Validator validator = context.getBean(name, Validator.class);
162164
assertSame(validator, adapter.getValidator());
163165
assertEquals(OptionalValidatorFactoryBean.class, validator.getClass());
166+
167+
WebBindingInitializer bindingInitializer = adapter.getWebBindingInitializer();
168+
assertNotNull(bindingInitializer);
169+
WebExchangeDataBinder binder = new WebExchangeDataBinder(new Object());
170+
bindingInitializer.initBinder(binder);
171+
assertSame(service, binder.getConversionService());
172+
assertSame(validator, binder.getValidator());
164173
}
165174

166175
@Test
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright 2002-2016 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+
* http://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+
package org.springframework.web.bind;
17+
18+
import java.util.Arrays;
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.TreeMap;
23+
24+
import reactor.core.publisher.Mono;
25+
26+
import org.springframework.beans.MutablePropertyValues;
27+
import org.springframework.core.ResolvableType;
28+
import org.springframework.http.MediaType;
29+
import org.springframework.http.codec.HttpMessageReader;
30+
import org.springframework.http.server.reactive.ServerHttpRequest;
31+
import org.springframework.util.LinkedMultiValueMap;
32+
import org.springframework.util.MultiValueMap;
33+
import org.springframework.web.multipart.MultipartFile;
34+
import org.springframework.web.server.ServerWebExchange;
35+
36+
/**
37+
* Specialized {@link org.springframework.validation.DataBinder} to perform data
38+
* binding from URL query params or form data in the request data to Java objects.
39+
*
40+
* @author Rossen Stoyanchev
41+
* @since 5.0
42+
*/
43+
public class WebExchangeDataBinder extends WebDataBinder {
44+
45+
private static final ResolvableType MULTIVALUE_MAP_TYPE = ResolvableType.forClass(MultiValueMap.class);
46+
47+
48+
private HttpMessageReader<MultiValueMap<String, String>> formReader = null;
49+
50+
51+
/**
52+
* Create a new instance, with default object name.
53+
* @param target the target object to bind onto (or {@code null} if the
54+
* binder is just used to convert a plain parameter value)
55+
* @see #DEFAULT_OBJECT_NAME
56+
*/
57+
public WebExchangeDataBinder(Object target) {
58+
super(target);
59+
}
60+
61+
/**
62+
* Create a new instance.
63+
* @param target the target object to bind onto (or {@code null} if the
64+
* binder is just used to convert a plain parameter value)
65+
* @param objectName the name of the target object
66+
*/
67+
public WebExchangeDataBinder(Object target, String objectName) {
68+
super(target, objectName);
69+
}
70+
71+
72+
public void setFormReader(HttpMessageReader<MultiValueMap<String, String>> formReader) {
73+
this.formReader = formReader;
74+
}
75+
76+
public HttpMessageReader<MultiValueMap<String, String>> getFormReader() {
77+
return this.formReader;
78+
}
79+
80+
81+
/**
82+
* Bind the URL query parameters or form data of the body of the given request
83+
* to this binder's target. The request body is parsed if the content-type
84+
* is "application/x-www-form-urlencoded".
85+
*
86+
* @param exchange the current exchange.
87+
* @return a {@code Mono<Void>} to indicate the result
88+
*/
89+
public Mono<Void> bind(ServerWebExchange exchange) {
90+
91+
ServerHttpRequest request = exchange.getRequest();
92+
Mono<MultiValueMap<String, String>> queryParams = Mono.just(request.getQueryParams());
93+
Mono<MultiValueMap<String, String>> formParams = getFormParams(exchange);
94+
95+
return Mono.zip(this::mergeParams, queryParams, formParams)
96+
.map(this::getParamsToBind)
97+
.doOnNext(values -> values.putAll(getMultipartFiles(exchange)))
98+
.doOnNext(values -> values.putAll(getExtraValuesToBind(exchange)))
99+
.then(values -> {
100+
doBind(new MutablePropertyValues(values));
101+
return Mono.empty();
102+
});
103+
}
104+
105+
private Mono<MultiValueMap<String, String>> getFormParams(ServerWebExchange exchange) {
106+
ServerHttpRequest request = exchange.getRequest();
107+
MediaType contentType = request.getHeaders().getContentType();
108+
if (this.formReader.canRead(MULTIVALUE_MAP_TYPE, contentType)) {
109+
return this.formReader.readMono(MULTIVALUE_MAP_TYPE, request, Collections.emptyMap());
110+
}
111+
else {
112+
return Mono.just(new LinkedMultiValueMap<>());
113+
}
114+
}
115+
116+
@SuppressWarnings("unchecked")
117+
private MultiValueMap<String, String> mergeParams(Object[] paramMaps) {
118+
MultiValueMap<String, String> result = new LinkedMultiValueMap<>();
119+
Arrays.stream(paramMaps).forEach(map -> result.putAll((MultiValueMap<String, String>) map));
120+
return result;
121+
}
122+
123+
private Map<String, Object> getParamsToBind(MultiValueMap<String, String> params) {
124+
Map<String, Object> valuesToBind = new TreeMap<>();
125+
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
126+
String name = entry.getKey();
127+
List<String> values = entry.getValue();
128+
if (values == null || values.isEmpty()) {
129+
// Do nothing, no values found at all.
130+
}
131+
else {
132+
if (values.size() > 1) {
133+
valuesToBind.put(name, values);
134+
}
135+
else {
136+
valuesToBind.put(name, values.get(0));
137+
}
138+
}
139+
}
140+
return valuesToBind;
141+
}
142+
143+
/**
144+
* Bind all multipart files contained in the given request, if any (in case
145+
* of a multipart request).
146+
* <p>Multipart files will only be added to the property values if they
147+
* are not empty or if we're configured to bind empty multipart files too.
148+
* @param exchange the current exchange
149+
* @return Map of field name String to MultipartFile object
150+
*/
151+
protected Map<String, List<MultipartFile>> getMultipartFiles(ServerWebExchange exchange) {
152+
// TODO
153+
return Collections.emptyMap();
154+
}
155+
156+
/**
157+
* Extension point that subclasses can use to add extra bind values for a
158+
* request. Invoked before {@link #doBind(MutablePropertyValues)}.
159+
* The default implementation is empty.
160+
* @param exchange the current exchange
161+
*/
162+
protected Map<String, ?> getExtraValuesToBind(ServerWebExchange exchange) {
163+
return Collections.emptyMap();
164+
}
165+
166+
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -22,7 +22,6 @@
2222
import org.springframework.validation.MessageCodesResolver;
2323
import org.springframework.validation.Validator;
2424
import org.springframework.web.bind.WebDataBinder;
25-
import org.springframework.web.context.request.WebRequest;
2625

2726
/**
2827
* Convenient {@link WebBindingInitializer} for declarative configuration
@@ -182,7 +181,7 @@ public final PropertyEditorRegistrar[] getPropertyEditorRegistrars() {
182181

183182

184183
@Override
185-
public void initBinder(WebDataBinder binder, WebRequest request) {
184+
public void initBinder(WebDataBinder binder) {
186185
binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
187186
if (this.directFieldAccess) {
188187
binder.initDirectFieldAccess();

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -47,6 +47,7 @@ public DefaultDataBinderFactory(WebBindingInitializer initializer) {
4747
* @throws Exception in case of invalid state or arguments
4848
*/
4949
@Override
50+
@SuppressWarnings("deprecation")
5051
public final WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName)
5152
throws Exception {
5253

@@ -74,7 +75,7 @@ protected WebDataBinder createBinderInstance(Object target, String objectName, N
7475

7576
/**
7677
* Extension point to further initialize the created data binder instance
77-
* (e.g. with {@code @InitBinder} methods) after "global" initializaton
78+
* (e.g. with {@code @InitBinder} methods) after "global" initialization
7879
* via {@link WebBindingInitializer}.
7980
* @param dataBinder the data binder instance to customize
8081
* @param webRequest the current request

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2007 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -20,19 +20,31 @@
2020
import org.springframework.web.context.request.WebRequest;
2121

2222
/**
23-
* Callback interface for initializing a {@link org.springframework.web.bind.WebDataBinder}
24-
* for performing data binding in the context of a specific web request.
23+
* Callback interface for initializing a {@link WebDataBinder} for performing
24+
* data binding in the context of a specific web request.
2525
*
2626
* @author Juergen Hoeller
27+
* @author Rossen Stoyanchev
2728
* @since 2.5
2829
*/
2930
public interface WebBindingInitializer {
3031

3132
/**
32-
* Initialize the given DataBinder for the given request.
33+
* Initialize the given DataBinder.
34+
* @param binder the DataBinder to initialize
35+
* @since 5.0
36+
*/
37+
void initBinder(WebDataBinder binder);
38+
39+
/**
40+
* Initialize the given DataBinder for the given (Servlet) request.
3341
* @param binder the DataBinder to initialize
3442
* @param request the web request that the data binding happens within
43+
* @deprecated as of 5.0 in favor of {@link #initBinder(WebDataBinder)}
3544
*/
36-
void initBinder(WebDataBinder binder, WebRequest request);
45+
@Deprecated
46+
default void initBinder(WebDataBinder binder, WebRequest request) {
47+
initBinder(binder);
48+
}
3749

3850
}

0 commit comments

Comments
 (0)