Skip to content

Commit 2d59433

Browse files
committed
Merge branch '6.2.x'
2 parents 9fe69e2 + 8aeced9 commit 2d59433

File tree

7 files changed

+351
-108
lines changed

7 files changed

+351
-108
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,120 @@
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+
import java.util.Set;
22+
import java.util.function.Predicate;
23+
24+
import reactor.core.publisher.Mono;
25+
26+
import org.springframework.http.HttpHeaders;
27+
import org.springframework.lang.Nullable;
28+
import org.springframework.util.CollectionUtils;
29+
import org.springframework.util.StringUtils;
30+
import org.springframework.web.bind.support.WebExchangeDataBinder;
31+
import org.springframework.web.reactive.HandlerMapping;
32+
import org.springframework.web.server.ServerWebExchange;
33+
34+
/**
35+
* Extended variant of {@link WebExchangeDataBinder} that adds URI path variables
36+
* and request headers to the bind values map.
37+
*
38+
* <p>Note: This class has existed since 5.0, but only as a private class within
39+
* {@link org.springframework.web.reactive.BindingContext}.
40+
*
41+
* @author Rossen Stoyanchev
42+
* @since 6.2.1
43+
*/
44+
public class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder {
45+
46+
private static final Set<String> FILTERED_HEADER_NAMES = Set.of("Priority");
47+
48+
49+
private Predicate<String> headerPredicate = name -> !FILTERED_HEADER_NAMES.contains(name);
50+
51+
52+
public ExtendedWebExchangeDataBinder(@Nullable Object target, String objectName) {
53+
super(target, objectName);
54+
}
55+
56+
57+
/**
58+
* Add a Predicate that filters the header names to use for data binding.
59+
* Multiple predicates are combined with {@code AND}.
60+
* @param headerPredicate the predicate to add
61+
* @since 6.2.1
62+
*/
63+
public void addHeaderPredicate(Predicate<String> headerPredicate) {
64+
this.headerPredicate = this.headerPredicate.and(headerPredicate);
65+
}
66+
67+
/**
68+
* Set the Predicate that filters the header names to use for data binding.
69+
* <p>Note that this method resets any previous predicates that may have been
70+
* set, including headers excluded by default such as the RFC 9218 defined
71+
* "Priority" header.
72+
* @param headerPredicate the predicate to add
73+
* @since 6.2.1
74+
*/
75+
public void setHeaderPredicate(Predicate<String> headerPredicate) {
76+
this.headerPredicate = headerPredicate;
77+
}
78+
79+
80+
@Override
81+
public Mono<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) {
82+
return super.getValuesToBind(exchange).doOnNext(map -> {
83+
Map<String, String> vars = exchange.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
84+
if (!CollectionUtils.isEmpty(vars)) {
85+
vars.forEach((key, value) -> addValueIfNotPresent(map, "URI variable", key, value));
86+
}
87+
HttpHeaders headers = exchange.getRequest().getHeaders();
88+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
89+
String name = entry.getKey();
90+
if (!this.headerPredicate.test(entry.getKey())) {
91+
continue;
92+
}
93+
List<String> values = entry.getValue();
94+
if (!CollectionUtils.isEmpty(values)) {
95+
// For constructor args with @BindParam mapped to the actual header name
96+
addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values));
97+
// Also adapt to Java conventions for setters
98+
name = StringUtils.uncapitalize(entry.getKey().replace("-", ""));
99+
addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values));
100+
}
101+
}
102+
});
103+
}
104+
105+
private static void addValueIfNotPresent(
106+
Map<String, Object> map, String label, String name, @Nullable Object value) {
107+
108+
if (value != null) {
109+
if (map.containsKey(name)) {
110+
if (logger.isDebugEnabled()) {
111+
logger.debug(label + " '" + name + "' overridden by request bind value.");
112+
}
113+
}
114+
else {
115+
map.put(name, value);
116+
}
117+
}
118+
}
119+
120+
}

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
}

0 commit comments

Comments
 (0)