Add headers to data binding values

Closes gh-32676
This commit is contained in:
rstoyanchev 2024-06-05 11:30:03 +01:00
parent 23160a43dd
commit f4f89aa2a4
6 changed files with 112 additions and 34 deletions

View File

@ -3,8 +3,8 @@
[.small]#xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Servlet stack]#
The `@ModelAttribute` method parameter annotation binds request parameters onto a model
object. For example:
The `@ModelAttribute` method parameter annotation binds form data, query parameters,
URI path variables, and request headers onto a model object. For example:
[tabs]
======
@ -27,6 +27,10 @@ Kotlin::
<1> Bind to an instance of `Pet`.
======
Form data and query parameters take precedence over URI variables and headers, which are
included only if they don't override request parameters with the same name. Dashes are
stripped from header names.
The `Pet` instance may be:
* Accessed from the model where it could have been added by a

View File

@ -3,8 +3,8 @@
[.small]#xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Reactive stack]#
The `@ModelAttribute` method parameter annotation binds request parameters onto a model
object. For example:
The `@ModelAttribute` method parameter annotation binds request parameters, URI path variables,
and request headers onto a model object. For example:
[tabs]
======
@ -31,7 +31,11 @@ fun processSubmit(@ModelAttribute pet: Pet): String { // <1>
<1> Bind to an instance of `Pet`.
======
The `Pet` instance may be:
Request parameters are a Servlet API concept that includes form data from the request body,
and query parameters. URI variables and headers are also included, but only if they don't
override request parameters with the same name. Dashes are stripped from header names.
The `Pet` instance above may be:
* Accessed from the model where it could have been added by a
xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method].

View File

@ -18,6 +18,7 @@ package org.springframework.web.reactive;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import reactor.core.publisher.Mono;
@ -26,6 +27,7 @@ import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils;
@ -214,6 +216,14 @@ public class BindingContext {
if (!CollectionUtils.isEmpty(vars)) {
vars.forEach((key, value) -> addValueIfNotPresent(map, "URI variable", key, value));
}
HttpHeaders headers = exchange.getRequest().getHeaders();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
List<String> values = entry.getValue();
if (!CollectionUtils.isEmpty(values)) {
String name = entry.getKey().replace("-", "");
addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values));
}
}
});
}

View File

@ -69,24 +69,50 @@ class BindingContextTests {
}
@Test
void uriVariablesAddedConditionally() {
void bindUriVariablesAndHeaders() {
MockServerHttpRequest request = MockServerHttpRequest.get("/path")
.header("Some-Int-Array", "1")
.header("Some-Int-Array", "2")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
exchange.getAttributes().put(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
Map.of("name", "John", "age", "25"));
TestBean target = new TestBean();
BindingContext bindingContext = new BindingContext(null);
WebExchangeDataBinder binder = bindingContext.createDataBinder(exchange, target, "testBean", null);
binder.bind(exchange).block();
assertThat(target.getName()).isEqualTo("John");
assertThat(target.getAge()).isEqualTo(25);
assertThat(target.getSomeIntArray()).containsExactly(1, 2);
}
@Test
void bindUriVarsAndHeadersAddedConditionally() {
MockServerHttpRequest request = MockServerHttpRequest.post("/path")
.header("name", "Johnny")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body("name=John&age=25");
MockServerWebExchange exchange = MockServerWebExchange.from(request);
exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Map.of("age", "26"));
TestBean testBean = new TestBean();
TestBean target = new TestBean();
BindingContext bindingContext = new BindingContext(null);
WebExchangeDataBinder binder = bindingContext.createDataBinder(exchange, testBean, "testBean", null);
WebExchangeDataBinder binder = bindingContext.createDataBinder(exchange, target, "testBean", null);
binder.bind(exchange).block();
assertThat(testBean.getName()).isEqualTo("John");
assertThat(testBean.getAge()).isEqualTo(25);
assertThat(target.getName()).isEqualTo("John");
assertThat(target.getAge()).isEqualTo(25);
}

View File

@ -16,10 +16,14 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.lang.Nullable;
@ -83,6 +87,17 @@ public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
if (uriVars != null) {
uriVars.forEach((name, value) -> addValueIfNotPresent(mpvs, "URI variable", name, value));
}
if (request instanceof HttpServletRequest httpRequest) {
Enumeration<String> names = httpRequest.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
Object value = getHeaderValue(httpRequest, name);
if (value != null) {
name = name.replace("-", "");
addValueIfNotPresent(mpvs, "Header", name, value);
}
}
}
}
@SuppressWarnings("unchecked")
@ -91,19 +106,35 @@ public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
return (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
}
private static void addValueIfNotPresent(
MutablePropertyValues mpvs, String label, String name, @Nullable Object value) {
if (value != null) {
if (mpvs.contains(name)) {
if (logger.isDebugEnabled()) {
logger.debug(label + " '" + name + "' overridden by request bind value.");
}
}
else {
mpvs.addPropertyValue(name, value);
private static void addValueIfNotPresent(MutablePropertyValues mpvs, String label, String name, Object value) {
if (mpvs.contains(name)) {
if (logger.isDebugEnabled()) {
logger.debug(label + " '" + name + "' overridden by request bind value.");
}
}
else {
mpvs.addPropertyValue(name, value);
}
}
@Nullable
private static Object getHeaderValue(HttpServletRequest request, String name) {
Enumeration<String> valuesEnum = request.getHeaders(name);
if (!valuesEnum.hasMoreElements()) {
return null;
}
String value = valuesEnum.nextElement();
if (!valuesEnum.hasMoreElements()) {
return value;
}
List<Object> values = new ArrayList<>();
values.add(value);
while (valuesEnum.hasMoreElements()) {
values.add(valuesEnum.nextElement());
}
return values;
}

View File

@ -16,7 +16,6 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
@ -38,41 +37,45 @@ class ExtendedServletRequestDataBinderTests {
private MockHttpServletRequest request;
@BeforeEach
void setup() {
this.request = new MockHttpServletRequest();
}
@Test
void createBinder() {
this.request.setAttribute(
request.setAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
Map.of("name", "nameValue", "age", "25"));
Map.of("name", "John", "age", "25"));
request.addHeader("Some-Int-Array", "1");
request.addHeader("Some-Int-Array", "2");
TestBean target = new TestBean();
ServletRequestDataBinder binder = new ExtendedServletRequestDataBinder(target, "");
binder.bind(request);
assertThat(target.getName()).isEqualTo("nameValue");
assertThat(target.getName()).isEqualTo("John");
assertThat(target.getAge()).isEqualTo(25);
assertThat(target.getSomeIntArray()).containsExactly(1, 2);
}
@Test
void uriTemplateVarAndRequestParam() {
request.addParameter("age", "35");
void uriVarsAndHeadersAddedConditionally() {
request.addParameter("name", "John");
request.addParameter("age", "25");
Map<String, String> uriTemplateVars = new HashMap<>();
uriTemplateVars.put("name", "nameValue");
uriTemplateVars.put("age", "25");
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars);
request.addHeader("name", "Johnny");
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Map.of("age", "26"));
TestBean target = new TestBean();
ServletRequestDataBinder binder = new ExtendedServletRequestDataBinder(target, "");
binder.bind(request);
assertThat(target.getName()).isEqualTo("nameValue");
assertThat(target.getAge()).isEqualTo(35);
assertThat(target.getName()).isEqualTo("John");
assertThat(target.getAge()).isEqualTo(25);
}
@Test