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 2a70b3f2d3e..6f9a4b95a56 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,19 +18,14 @@ 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; - 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; import org.springframework.validation.BindingResult; import org.springframework.validation.DataBinder; import org.springframework.validation.SmartValidator; @@ -141,7 +136,7 @@ public class BindingContext { public WebExchangeDataBinder createDataBinder( ServerWebExchange exchange, @Nullable Object target, String name, @Nullable ResolvableType targetType) { - WebExchangeDataBinder dataBinder = new ExtendedWebExchangeDataBinder(target, name); + WebExchangeDataBinder dataBinder = createBinderInstance(target, name); dataBinder.setNameResolver(new BindParamNameResolver()); if (target == null && targetType != null) { @@ -163,6 +158,18 @@ public class BindingContext { return dataBinder; } + /** + * Extension point to create the WebDataBinder instance. + * By default, this is {@code WebRequestDataBinder}. + * @param target the binding target or {@code null} for type conversion only + * @param name the binding target object name + * @return the created {@link WebExchangeDataBinder} instance + * @since 6.2.1 + */ + protected WebExchangeDataBinder createBinderInstance(@Nullable Object target, String name) { + return new WebExchangeDataBinder(target, name); + } + /** * Initialize the data binder instance for the given exchange. * @throws ServerErrorException if {@code @InitBinder} method invocation fails @@ -200,51 +207,6 @@ public class BindingContext { } - /** - * Extended variant of {@link WebExchangeDataBinder}, adding path variables. - */ - private static class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder { - - public ExtendedWebExchangeDataBinder(@Nullable Object target, String objectName) { - super(target, objectName); - } - - @Override - public Mono> getValuesToBind(ServerWebExchange exchange) { - return super.getValuesToBind(exchange).doOnNext(map -> { - Map vars = exchange.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); - 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)); - } - } - }); - } - - private static void addValueIfNotPresent( - Map map, String label, String name, @Nullable Object value) { - - if (value != null) { - if (map.containsKey(name)) { - if (logger.isDebugEnabled()) { - logger.debug(label + " '" + name + "' overridden by request bind value."); - } - } - else { - map.put(name, value); - } - } - } - - } - - /** * Excludes Bean Validation if the method parameter has {@code @Valid}. */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java new file mode 100644 index 00000000000..0863499d867 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.support.WebExchangeDataBinder; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * Extended variant of {@link WebExchangeDataBinder} that adds URI path variables + * and request headers to the bind values map. + * + *

Note: This class has existed since 5.0, but only as a private class within + * {@link org.springframework.web.reactive.BindingContext}. + * + * @author Rossen Stoyanchev + * @since 6.2.1 + */ +public class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder { + + + public ExtendedWebExchangeDataBinder(@Nullable Object target, String objectName) { + super(target, objectName); + } + + + @Override + public Mono> getValuesToBind(ServerWebExchange exchange) { + return super.getValuesToBind(exchange).doOnNext(map -> { + Map vars = exchange.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + 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)); + } + } + }); + } + + private static void addValueIfNotPresent( + Map map, String label, String name, @Nullable Object value) { + + if (value != null) { + if (map.containsKey(name)) { + if (logger.isDebugEnabled()) { + logger.debug(label + " '" + name + "' overridden by request bind value."); + } + } + else { + map.put(name, value); + } + } + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java index 8fa00f33d5e..c8d67038c64 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,6 +71,15 @@ class InitBinderBindingContext extends BindingContext { } + /** + * Returns an instance of {@link ExtendedWebExchangeDataBinder}. + * @since 6.2.1 + */ + @Override + protected WebExchangeDataBinder createBinderInstance(@Nullable Object target, String name) { + return new ExtendedWebExchangeDataBinder(target, name); + } + @Override protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder dataBinder, ServerWebExchange exchange) { this.binderMethods.stream() 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 995cc00648d..dab582a98c2 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 @@ -17,20 +17,16 @@ package org.springframework.web.reactive; import java.lang.reflect.Method; -import java.util.Map; import jakarta.validation.Valid; import org.junit.jupiter.api.Test; -import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.ResolvableType; -import org.springframework.http.MediaType; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; import org.springframework.web.testfixture.server.MockServerWebExchange; @@ -68,54 +64,6 @@ class BindingContextTests { assertThat(binder.getValidatorsToApply()).containsExactly(springValidator); } - @Test - 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 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); - } - - @SuppressWarnings("unused") private void handleValidObject(@Valid Foo foo) { } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java index 52557547f01..b12f755ec6a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java @@ -20,18 +20,23 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; +import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.http.MediaType; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.reactive.BindingContext; +import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver; import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; @@ -123,6 +128,52 @@ class InitBinderBindingContextTests { assertThat(dataBinder.getDisallowedFields()[0]).isEqualToIgnoringCase("requestParam-22"); } + @Test + void bindUriVariablesAndHeaders() throws Exception { + + 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 context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class); + WebExchangeDataBinder binder = context.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() throws Exception { + + 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 target = new TestBean(); + + BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class); + WebExchangeDataBinder binder = context.createDataBinder(exchange, target, "testBean", null); + + binder.bind(exchange).block(); + + assertThat(target.getName()).isEqualTo("John"); + assertThat(target.getAge()).isEqualTo(25); + } private BindingContext createBindingContext(String methodName, Class... parameterTypes) throws Exception { Object handler = new InitBinderHandler();