Skip to content

Commit 7b4e19c

Browse files
committed
Make ExtendedServletRequestDataBinder public
Make it public and move it down to the annotations package alongside InitBinderBindingContext. This is mirrors the hierarchy in Spring MVC with the ExtendedServletRequestDataBinder. The change will allow customization of the header names to include/exclude in data binding. See gh-34039
1 parent 3b95d2c commit 7b4e19c

File tree

5 files changed

+156
-104
lines changed

5 files changed

+156
-104
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java

+13-51
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,14 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.util.Collection;
21-
import java.util.List;
2221
import java.util.Map;
2322

24-
import reactor.core.publisher.Mono;
25-
2623
import org.springframework.beans.BeanUtils;
2724
import org.springframework.core.MethodParameter;
2825
import org.springframework.core.ReactiveAdapterRegistry;
2926
import org.springframework.core.ResolvableType;
30-
import org.springframework.http.HttpHeaders;
3127
import org.springframework.lang.Nullable;
3228
import org.springframework.ui.Model;
33-
import org.springframework.util.CollectionUtils;
3429
import org.springframework.validation.BindingResult;
3530
import org.springframework.validation.DataBinder;
3631
import org.springframework.validation.SmartValidator;
@@ -141,7 +136,7 @@ public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, String
141136
public WebExchangeDataBinder createDataBinder(
142137
ServerWebExchange exchange, @Nullable Object target, String name, @Nullable ResolvableType targetType) {
143138

144-
WebExchangeDataBinder dataBinder = new ExtendedWebExchangeDataBinder(target, name);
139+
WebExchangeDataBinder dataBinder = createBinderInstance(target, name);
145140
dataBinder.setNameResolver(new BindParamNameResolver());
146141

147142
if (target == null && targetType != null) {
@@ -163,6 +158,18 @@ public WebExchangeDataBinder createDataBinder(
163158
return dataBinder;
164159
}
165160

161+
/**
162+
* Extension point to create the WebDataBinder instance.
163+
* By default, this is {@code WebRequestDataBinder}.
164+
* @param target the binding target or {@code null} for type conversion only
165+
* @param name the binding target object name
166+
* @return the created {@link WebExchangeDataBinder} instance
167+
* @since 6.2.1
168+
*/
169+
protected WebExchangeDataBinder createBinderInstance(@Nullable Object target, String name) {
170+
return new WebExchangeDataBinder(target, name);
171+
}
172+
166173
/**
167174
* Initialize the data binder instance for the given exchange.
168175
* @throws ServerErrorException if {@code @InitBinder} method invocation fails
@@ -200,51 +207,6 @@ private boolean isBindingCandidate(String name, @Nullable Object value) {
200207
}
201208

202209

203-
/**
204-
* Extended variant of {@link WebExchangeDataBinder}, adding path variables.
205-
*/
206-
private static class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder {
207-
208-
public ExtendedWebExchangeDataBinder(@Nullable Object target, String objectName) {
209-
super(target, objectName);
210-
}
211-
212-
@Override
213-
public Mono<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) {
214-
return super.getValuesToBind(exchange).doOnNext(map -> {
215-
Map<String, String> vars = exchange.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
216-
if (!CollectionUtils.isEmpty(vars)) {
217-
vars.forEach((key, value) -> addValueIfNotPresent(map, "URI variable", key, value));
218-
}
219-
HttpHeaders headers = exchange.getRequest().getHeaders();
220-
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
221-
List<String> values = entry.getValue();
222-
if (!CollectionUtils.isEmpty(values)) {
223-
String name = entry.getKey().replace("-", "");
224-
addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values));
225-
}
226-
}
227-
});
228-
}
229-
230-
private static void addValueIfNotPresent(
231-
Map<String, Object> map, String label, String name, @Nullable Object value) {
232-
233-
if (value != null) {
234-
if (map.containsKey(name)) {
235-
if (logger.isDebugEnabled()) {
236-
logger.debug(label + " '" + name + "' overridden by request bind value.");
237-
}
238-
}
239-
else {
240-
map.put(name, value);
241-
}
242-
}
243-
}
244-
245-
}
246-
247-
248210
/**
249211
* Excludes Bean Validation if the method parameter has {@code @Valid}.
250212
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2002-2024 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.reactive.result.method.annotation;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
import reactor.core.publisher.Mono;
23+
24+
import org.springframework.http.HttpHeaders;
25+
import org.springframework.lang.Nullable;
26+
import org.springframework.util.CollectionUtils;
27+
import org.springframework.web.bind.support.WebExchangeDataBinder;
28+
import org.springframework.web.reactive.HandlerMapping;
29+
import org.springframework.web.server.ServerWebExchange;
30+
31+
/**
32+
* Extended variant of {@link WebExchangeDataBinder} that adds URI path variables
33+
* and request headers to the bind values map.
34+
*
35+
* <p>Note: This class has existed since 5.0, but only as a private class within
36+
* {@link org.springframework.web.reactive.BindingContext}.
37+
*
38+
* @author Rossen Stoyanchev
39+
* @since 6.2.1
40+
*/
41+
public class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder {
42+
43+
44+
public ExtendedWebExchangeDataBinder(@Nullable Object target, String objectName) {
45+
super(target, objectName);
46+
}
47+
48+
49+
@Override
50+
public Mono<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) {
51+
return super.getValuesToBind(exchange).doOnNext(map -> {
52+
Map<String, String> vars = exchange.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
53+
if (!CollectionUtils.isEmpty(vars)) {
54+
vars.forEach((key, value) -> addValueIfNotPresent(map, "URI variable", key, value));
55+
}
56+
HttpHeaders headers = exchange.getRequest().getHeaders();
57+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
58+
List<String> values = entry.getValue();
59+
if (!CollectionUtils.isEmpty(values)) {
60+
String name = entry.getKey().replace("-", "");
61+
addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values));
62+
}
63+
}
64+
});
65+
}
66+
67+
private static void addValueIfNotPresent(
68+
Map<String, Object> map, String label, String name, @Nullable Object value) {
69+
70+
if (value != null) {
71+
if (map.containsKey(name)) {
72+
if (logger.isDebugEnabled()) {
73+
logger.debug(label + " '" + name + "' overridden by request bind value.");
74+
}
75+
}
76+
else {
77+
map.put(name, value);
78+
}
79+
}
80+
}
81+
82+
}

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -71,6 +71,15 @@ public SessionStatus getSessionStatus() {
7171
}
7272

7373

74+
/**
75+
* Returns an instance of {@link ExtendedWebExchangeDataBinder}.
76+
* @since 6.2.1
77+
*/
78+
@Override
79+
protected WebExchangeDataBinder createBinderInstance(@Nullable Object target, String name) {
80+
return new ExtendedWebExchangeDataBinder(target, name);
81+
}
82+
7483
@Override
7584
protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder dataBinder, ServerWebExchange exchange) {
7685
this.binderMethods.stream()

spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java

-52
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,16 @@
1717
package org.springframework.web.reactive;
1818

1919
import java.lang.reflect.Method;
20-
import java.util.Map;
2120

2221
import jakarta.validation.Valid;
2322
import org.junit.jupiter.api.Test;
2423

25-
import org.springframework.beans.testfixture.beans.TestBean;
2624
import org.springframework.core.ResolvableType;
27-
import org.springframework.http.MediaType;
2825
import org.springframework.validation.Errors;
2926
import org.springframework.validation.SmartValidator;
3027
import org.springframework.validation.Validator;
3128
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
3229
import org.springframework.web.bind.WebDataBinder;
33-
import org.springframework.web.bind.support.WebExchangeDataBinder;
3430
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
3531
import org.springframework.web.testfixture.server.MockServerWebExchange;
3632

@@ -68,54 +64,6 @@ void jakartaValidatorExcludedWhenMethodValidationApplicable() throws Exception {
6864
assertThat(binder.getValidatorsToApply()).containsExactly(springValidator);
6965
}
7066

71-
@Test
72-
void bindUriVariablesAndHeaders() {
73-
74-
MockServerHttpRequest request = MockServerHttpRequest.get("/path")
75-
.header("Some-Int-Array", "1")
76-
.header("Some-Int-Array", "2")
77-
.build();
78-
79-
MockServerWebExchange exchange = MockServerWebExchange.from(request);
80-
exchange.getAttributes().put(
81-
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
82-
Map.of("name", "John", "age", "25"));
83-
84-
TestBean target = new TestBean();
85-
86-
BindingContext bindingContext = new BindingContext(null);
87-
WebExchangeDataBinder binder = bindingContext.createDataBinder(exchange, target, "testBean", null);
88-
89-
binder.bind(exchange).block();
90-
91-
assertThat(target.getName()).isEqualTo("John");
92-
assertThat(target.getAge()).isEqualTo(25);
93-
assertThat(target.getSomeIntArray()).containsExactly(1, 2);
94-
}
95-
96-
@Test
97-
void bindUriVarsAndHeadersAddedConditionally() {
98-
99-
MockServerHttpRequest request = MockServerHttpRequest.post("/path")
100-
.header("name", "Johnny")
101-
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
102-
.body("name=John&age=25");
103-
104-
MockServerWebExchange exchange = MockServerWebExchange.from(request);
105-
exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Map.of("age", "26"));
106-
107-
TestBean target = new TestBean();
108-
109-
BindingContext bindingContext = new BindingContext(null);
110-
WebExchangeDataBinder binder = bindingContext.createDataBinder(exchange, target, "testBean", null);
111-
112-
binder.bind(exchange).block();
113-
114-
assertThat(target.getName()).isEqualTo("John");
115-
assertThat(target.getAge()).isEqualTo(25);
116-
}
117-
118-
11967
@SuppressWarnings("unused")
12068
private void handleValidObject(@Valid Foo foo) {
12169
}

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java

+51
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,23 @@
2020
import java.util.ArrayList;
2121
import java.util.Collections;
2222
import java.util.List;
23+
import java.util.Map;
2324

2425
import org.junit.jupiter.api.Test;
2526

27+
import org.springframework.beans.testfixture.beans.TestBean;
2628
import org.springframework.core.DefaultParameterNameDiscoverer;
2729
import org.springframework.core.ReactiveAdapterRegistry;
2830
import org.springframework.core.convert.ConversionService;
2931
import org.springframework.format.support.DefaultFormattingConversionService;
32+
import org.springframework.http.MediaType;
3033
import org.springframework.web.bind.WebDataBinder;
3134
import org.springframework.web.bind.annotation.InitBinder;
3235
import org.springframework.web.bind.annotation.RequestParam;
3336
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
37+
import org.springframework.web.bind.support.WebExchangeDataBinder;
3438
import org.springframework.web.reactive.BindingContext;
39+
import org.springframework.web.reactive.HandlerMapping;
3540
import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
3641
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod;
3742
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
@@ -123,6 +128,52 @@ void createBinderTypeConversion() throws Exception {
123128
assertThat(dataBinder.getDisallowedFields()[0]).isEqualToIgnoringCase("requestParam-22");
124129
}
125130

131+
@Test
132+
void bindUriVariablesAndHeaders() throws Exception {
133+
134+
MockServerHttpRequest request = MockServerHttpRequest.get("/path")
135+
.header("Some-Int-Array", "1")
136+
.header("Some-Int-Array", "2")
137+
.build();
138+
139+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
140+
exchange.getAttributes().put(
141+
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
142+
Map.of("name", "John", "age", "25"));
143+
144+
TestBean target = new TestBean();
145+
146+
BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class);
147+
WebExchangeDataBinder binder = context.createDataBinder(exchange, target, "testBean", null);
148+
149+
binder.bind(exchange).block();
150+
151+
assertThat(target.getName()).isEqualTo("John");
152+
assertThat(target.getAge()).isEqualTo(25);
153+
assertThat(target.getSomeIntArray()).containsExactly(1, 2);
154+
}
155+
156+
@Test
157+
void bindUriVarsAndHeadersAddedConditionally() throws Exception {
158+
159+
MockServerHttpRequest request = MockServerHttpRequest.post("/path")
160+
.header("name", "Johnny")
161+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
162+
.body("name=John&age=25");
163+
164+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
165+
exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Map.of("age", "26"));
166+
167+
TestBean target = new TestBean();
168+
169+
BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class);
170+
WebExchangeDataBinder binder = context.createDataBinder(exchange, target, "testBean", null);
171+
172+
binder.bind(exchange).block();
173+
174+
assertThat(target.getName()).isEqualTo("John");
175+
assertThat(target.getAge()).isEqualTo(25);
176+
}
126177

127178
private BindingContext createBindingContext(String methodName, Class<?>... parameterTypes) throws Exception {
128179
Object handler = new InitBinderHandler();

0 commit comments

Comments
 (0)