Browse Source

Add headers to data binding values

Closes gh-32676
pull/32966/head
rstoyanchev 2 years ago
parent
commit
f4f89aa2a4
  1. 8
      framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc
  2. 10
      framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc
  3. 10
      spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java
  4. 36
      spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java
  5. 53
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java
  6. 29
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java

8
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]# [.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 The `@ModelAttribute` method parameter annotation binds form data, query parameters,
object. For example: URI path variables, and request headers onto a model object. For example:
[tabs] [tabs]
====== ======
@ -27,6 +27,10 @@ Kotlin::
<1> Bind to an instance of `Pet`. <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: The `Pet` instance may be:
* Accessed from the model where it could have been added by a * Accessed from the model where it could have been added by a

10
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]# [.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 The `@ModelAttribute` method parameter annotation binds request parameters, URI path variables,
object. For example: and request headers onto a model object. For example:
[tabs] [tabs]
====== ======
@ -31,7 +31,11 @@ fun processSubmit(@ModelAttribute pet: Pet): String { // <1>
<1> Bind to an instance of `Pet`. <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 * Accessed from the model where it could have been added by a
xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method]. xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method].

10
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.lang.annotation.Annotation;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Map; import java.util.Map;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -26,6 +27,7 @@ import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
@ -214,6 +216,14 @@ public class BindingContext {
if (!CollectionUtils.isEmpty(vars)) { if (!CollectionUtils.isEmpty(vars)) {
vars.forEach((key, value) -> addValueIfNotPresent(map, "URI variable", key, value)); 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));
}
}
}); });
} }

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

@ -69,24 +69,50 @@ class BindingContextTests {
} }
@Test @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") MockServerHttpRequest request = MockServerHttpRequest.post("/path")
.header("name", "Johnny")
.contentType(MediaType.APPLICATION_FORM_URLENCODED) .contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body("name=John&age=25"); .body("name=John&age=25");
MockServerWebExchange exchange = MockServerWebExchange.from(request); MockServerWebExchange exchange = MockServerWebExchange.from(request);
exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Map.of("age", "26")); 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); 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(); binder.bind(exchange).block();
assertThat(testBean.getName()).isEqualTo("John"); assertThat(target.getName()).isEqualTo("John");
assertThat(testBean.getAge()).isEqualTo(25); assertThat(target.getAge()).isEqualTo(25);
} }

53
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; 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.Map;
import java.util.Set; import java.util.Set;
import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.MutablePropertyValues;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -83,6 +87,17 @@ public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
if (uriVars != null) { if (uriVars != null) {
uriVars.forEach((name, value) -> addValueIfNotPresent(mpvs, "URI variable", name, value)); 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") @SuppressWarnings("unchecked")
@ -91,19 +106,35 @@ public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
return (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); return (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
} }
private static void addValueIfNotPresent( private static void addValueIfNotPresent(MutablePropertyValues mpvs, String label, String name, Object value) {
MutablePropertyValues mpvs, String label, String name, @Nullable Object value) { if (mpvs.contains(name)) {
if (logger.isDebugEnabled()) {
if (value != null) { logger.debug(label + " '" + name + "' overridden by request bind value.");
if (mpvs.contains(name)) {
if (logger.isDebugEnabled()) {
logger.debug(label + " '" + name + "' overridden by request bind value.");
}
}
else {
mpvs.addPropertyValue(name, 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;
} }

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

Loading…
Cancel
Save