From f4f89aa2a405c58f85f8517e763c7ea440351e22 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 5 Jun 2024 11:30:03 +0100 Subject: [PATCH] Add headers to data binding values Closes gh-32676 --- .../ann-methods/modelattrib-method-args.adoc | 8 ++- .../ann-methods/modelattrib-method-args.adoc | 10 ++-- .../web/reactive/BindingContext.java | 10 ++++ .../web/reactive/BindingContextTests.java | 36 +++++++++++-- .../ExtendedServletRequestDataBinder.java | 53 +++++++++++++++---- ...ExtendedServletRequestDataBinderTests.java | 29 +++++----- 6 files changed, 112 insertions(+), 34 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc index 45977c0b9e6..a25ec608fe8 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc @@ -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 diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc index 1ad2640d2ab..20b5ae436b0 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc @@ -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]. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java b/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java index fa0a4aefe2f..2a70b3f2d3e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java @@ -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> entry : headers.entrySet()) { + List values = entry.getValue(); + if (!CollectionUtils.isEmpty(values)) { + String name = entry.getKey().replace("-", ""); + addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values)); + } + } }); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java index 7ffc03234fd..995cc00648d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java @@ -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); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java index f185f85a09e..4d4e26a131a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java @@ -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 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) 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 valuesEnum = request.getHeaders(name); + if (!valuesEnum.hasMoreElements()) { + return null; + } + + String value = valuesEnum.nextElement(); + if (!valuesEnum.hasMoreElements()) { + return value; + } + + List values = new ArrayList<>(); + values.add(value); + while (valuesEnum.hasMoreElements()) { + values.add(valuesEnum.nextElement()); + } + return values; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java index f3cb0a903a4..83f64ca1b8c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java @@ -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 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