Skip to content

Commit f4f89aa

Browse files
committed
Add headers to data binding values
Closes gh-32676
1 parent 23160a4 commit f4f89aa

File tree

6 files changed

+112
-34
lines changed

6 files changed

+112
-34
lines changed

framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc

+6-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
[.small]#xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Servlet stack]#
55

6-
The `@ModelAttribute` method parameter annotation binds request parameters onto a model
7-
object. For example:
6+
The `@ModelAttribute` method parameter annotation binds form data, query parameters,
7+
URI path variables, and request headers onto a model object. For example:
88

99
[tabs]
1010
======
@@ -27,6 +27,10 @@ Kotlin::
2727
<1> Bind to an instance of `Pet`.
2828
======
2929

30+
Form data and query parameters take precedence over URI variables and headers, which are
31+
included only if they don't override request parameters with the same name. Dashes are
32+
stripped from header names.
33+
3034
The `Pet` instance may be:
3135

3236
* Accessed from the model where it could have been added by a

framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc

+7-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
[.small]#xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Reactive stack]#
55

6-
The `@ModelAttribute` method parameter annotation binds request parameters onto a model
7-
object. For example:
6+
The `@ModelAttribute` method parameter annotation binds request parameters, URI path variables,
7+
and request headers onto a model object. For example:
88

99
[tabs]
1010
======
@@ -31,7 +31,11 @@ fun processSubmit(@ModelAttribute pet: Pet): String { // <1>
3131
<1> Bind to an instance of `Pet`.
3232
======
3333

34-
The `Pet` instance may be:
34+
Request parameters are a Servlet API concept that includes form data from the request body,
35+
and query parameters. URI variables and headers are also included, but only if they don't
36+
override request parameters with the same name. Dashes are stripped from header names.
37+
38+
The `Pet` instance above may be:
3539

3640
* Accessed from the model where it could have been added by a
3741
xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method].

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

+10
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.annotation.Annotation;
2020
import java.util.Collection;
21+
import java.util.List;
2122
import java.util.Map;
2223

2324
import reactor.core.publisher.Mono;
@@ -26,6 +27,7 @@
2627
import org.springframework.core.MethodParameter;
2728
import org.springframework.core.ReactiveAdapterRegistry;
2829
import org.springframework.core.ResolvableType;
30+
import org.springframework.http.HttpHeaders;
2931
import org.springframework.lang.Nullable;
3032
import org.springframework.ui.Model;
3133
import org.springframework.util.CollectionUtils;
@@ -214,6 +216,14 @@ public Mono<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) {
214216
if (!CollectionUtils.isEmpty(vars)) {
215217
vars.forEach((key, value) -> addValueIfNotPresent(map, "URI variable", key, value));
216218
}
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+
}
217227
});
218228
}
219229

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

+31-5
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,50 @@ void jakartaValidatorExcludedWhenMethodValidationApplicable() throws Exception {
6969
}
7070

7171
@Test
72-
void uriVariablesAddedConditionally() {
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() {
7398

7499
MockServerHttpRequest request = MockServerHttpRequest.post("/path")
100+
.header("name", "Johnny")
75101
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
76102
.body("name=John&age=25");
77103

78104
MockServerWebExchange exchange = MockServerWebExchange.from(request);
79105
exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Map.of("age", "26"));
80106

81-
TestBean testBean = new TestBean();
107+
TestBean target = new TestBean();
82108

83109
BindingContext bindingContext = new BindingContext(null);
84-
WebExchangeDataBinder binder = bindingContext.createDataBinder(exchange, testBean, "testBean", null);
110+
WebExchangeDataBinder binder = bindingContext.createDataBinder(exchange, target, "testBean", null);
85111

86112
binder.bind(exchange).block();
87113

88-
assertThat(testBean.getName()).isEqualTo("John");
89-
assertThat(testBean.getAge()).isEqualTo(25);
114+
assertThat(target.getName()).isEqualTo("John");
115+
assertThat(target.getAge()).isEqualTo(25);
90116
}
91117

92118

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java

+42-11
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616

1717
package org.springframework.web.servlet.mvc.method.annotation;
1818

19+
import java.util.ArrayList;
20+
import java.util.Enumeration;
21+
import java.util.List;
1922
import java.util.Map;
2023
import java.util.Set;
2124

2225
import jakarta.servlet.ServletRequest;
26+
import jakarta.servlet.http.HttpServletRequest;
2327

2428
import org.springframework.beans.MutablePropertyValues;
2529
import org.springframework.lang.Nullable;
@@ -83,6 +87,17 @@ protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request)
8387
if (uriVars != null) {
8488
uriVars.forEach((name, value) -> addValueIfNotPresent(mpvs, "URI variable", name, value));
8589
}
90+
if (request instanceof HttpServletRequest httpRequest) {
91+
Enumeration<String> names = httpRequest.getHeaderNames();
92+
while (names.hasMoreElements()) {
93+
String name = names.nextElement();
94+
Object value = getHeaderValue(httpRequest, name);
95+
if (value != null) {
96+
name = name.replace("-", "");
97+
addValueIfNotPresent(mpvs, "Header", name, value);
98+
}
99+
}
100+
}
86101
}
87102

88103
@SuppressWarnings("unchecked")
@@ -91,19 +106,35 @@ private static Map<String, String> getUriVars(ServletRequest request) {
91106
return (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
92107
}
93108

94-
private static void addValueIfNotPresent(
95-
MutablePropertyValues mpvs, String label, String name, @Nullable Object value) {
96-
97-
if (value != null) {
98-
if (mpvs.contains(name)) {
99-
if (logger.isDebugEnabled()) {
100-
logger.debug(label + " '" + name + "' overridden by request bind value.");
101-
}
102-
}
103-
else {
104-
mpvs.addPropertyValue(name, value);
109+
private static void addValueIfNotPresent(MutablePropertyValues mpvs, String label, String name, Object value) {
110+
if (mpvs.contains(name)) {
111+
if (logger.isDebugEnabled()) {
112+
logger.debug(label + " '" + name + "' overridden by request bind value.");
105113
}
106114
}
115+
else {
116+
mpvs.addPropertyValue(name, value);
117+
}
118+
}
119+
120+
@Nullable
121+
private static Object getHeaderValue(HttpServletRequest request, String name) {
122+
Enumeration<String> valuesEnum = request.getHeaders(name);
123+
if (!valuesEnum.hasMoreElements()) {
124+
return null;
125+
}
126+
127+
String value = valuesEnum.nextElement();
128+
if (!valuesEnum.hasMoreElements()) {
129+
return value;
130+
}
131+
132+
List<Object> values = new ArrayList<>();
133+
values.add(value);
134+
while (valuesEnum.hasMoreElements()) {
135+
values.add(valuesEnum.nextElement());
136+
}
137+
return values;
107138
}
108139

109140

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java

+16-13
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.web.servlet.mvc.method.annotation;
1818

19-
import java.util.HashMap;
2019
import java.util.Map;
2120

2221
import org.junit.jupiter.api.BeforeEach;
@@ -38,41 +37,45 @@ class ExtendedServletRequestDataBinderTests {
3837

3938
private MockHttpServletRequest request;
4039

40+
4141
@BeforeEach
4242
void setup() {
4343
this.request = new MockHttpServletRequest();
4444
}
4545

46+
4647
@Test
4748
void createBinder() {
48-
49-
this.request.setAttribute(
49+
request.setAttribute(
5050
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
51-
Map.of("name", "nameValue", "age", "25"));
51+
Map.of("name", "John", "age", "25"));
52+
53+
request.addHeader("Some-Int-Array", "1");
54+
request.addHeader("Some-Int-Array", "2");
5255

5356
TestBean target = new TestBean();
5457
ServletRequestDataBinder binder = new ExtendedServletRequestDataBinder(target, "");
5558
binder.bind(request);
5659

57-
assertThat(target.getName()).isEqualTo("nameValue");
60+
assertThat(target.getName()).isEqualTo("John");
5861
assertThat(target.getAge()).isEqualTo(25);
62+
assertThat(target.getSomeIntArray()).containsExactly(1, 2);
5963
}
6064

6165
@Test
62-
void uriTemplateVarAndRequestParam() {
63-
request.addParameter("age", "35");
66+
void uriVarsAndHeadersAddedConditionally() {
67+
request.addParameter("name", "John");
68+
request.addParameter("age", "25");
6469

65-
Map<String, String> uriTemplateVars = new HashMap<>();
66-
uriTemplateVars.put("name", "nameValue");
67-
uriTemplateVars.put("age", "25");
68-
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars);
70+
request.addHeader("name", "Johnny");
71+
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Map.of("age", "26"));
6972

7073
TestBean target = new TestBean();
7174
ServletRequestDataBinder binder = new ExtendedServletRequestDataBinder(target, "");
7275
binder.bind(request);
7376

74-
assertThat(target.getName()).isEqualTo("nameValue");
75-
assertThat(target.getAge()).isEqualTo(35);
77+
assertThat(target.getName()).isEqualTo("John");
78+
assertThat(target.getAge()).isEqualTo(25);
7679
}
7780

7881
@Test

0 commit comments

Comments
 (0)