From c6810334cac77ec54cab2ff8030d397eb864e50a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 30 Nov 2025 11:31:04 +0100 Subject: [PATCH] Move extended web data binders to common support packages This resolves cyclic dependencies between function and annotation-based packages. See gh-35800 --- .../function/server/DefaultServerRequest.java | 2 +- .../result/ExtendedWebExchangeDataBinder.java | 132 +++++++++++ .../ExtendedWebExchangeDataBinder.java | 99 +------- .../annotation/InitBinderBindingContext.java | 4 +- .../InitBinderBindingContextTests.java | 8 +- .../function/DefaultServerRequest.java | 2 +- .../ExtendedServletRequestDataBinder.java | 164 +------------ .../ServletRequestDataBinderFactory.java | 4 +- .../ExtendedServletRequestDataBinder.java | 223 ++++++++++++++++++ ...ExtendedServletRequestDataBinderTests.java | 2 +- 10 files changed, 376 insertions(+), 264 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/result/ExtendedWebExchangeDataBinder.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/support/ExtendedServletRequestDataBinder.java rename spring-webmvc/src/test/java/org/springframework/web/servlet/{mvc/method/annotation => support}/ExtendedServletRequestDataBinderTests.java (98%) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java index 9a6aef18f82..2ed01d4c5d2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequest.java @@ -61,7 +61,7 @@ import org.springframework.web.reactive.accept.ApiVersionStrategy; import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.UnsupportedMediaTypeException; -import org.springframework.web.reactive.result.method.annotation.ExtendedWebExchangeDataBinder; +import org.springframework.web.reactive.result.ExtendedWebExchangeDataBinder; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.UnsupportedMediaTypeStatusException; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/ExtendedWebExchangeDataBinder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/ExtendedWebExchangeDataBinder.java new file mode 100644 index 00000000000..9090db57171 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/ExtendedWebExchangeDataBinder.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-present 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; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import org.jspecify.annotations.Nullable; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +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. + * + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @since 7.0.2 + * @see WebExchangeDataBinder + * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE + */ +public class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder { + + private static final Set FILTERED_HEADER_NAMES = Set.of("accept", "authorization", "connection", + "cookie", "from", "host", "origin", "priority", "range", "referer", "upgrade"); + + + private Predicate headerPredicate = name -> !FILTERED_HEADER_NAMES.contains(name.toLowerCase(Locale.ROOT)); + + + /** + * Create a new instance, with default object name. + * @param target the target object to bind onto (or {@code null} if the + * binder is just used to convert a plain parameter value) + * @see #DEFAULT_OBJECT_NAME + */ + public ExtendedWebExchangeDataBinder(@Nullable Object target) { + super(target); + } + + public ExtendedWebExchangeDataBinder(@Nullable Object target, String objectName) { + super(target, objectName); + } + + + /** + * Add a Predicate that filters the header names to use for data binding. + * Multiple predicates are combined with {@code AND}. + * @param headerPredicate the predicate to add + * @since 6.2.1 + */ + public void addHeaderPredicate(Predicate headerPredicate) { + this.headerPredicate = this.headerPredicate.and(headerPredicate); + } + + /** + * Set the Predicate that filters the header names to use for data binding. + *

Note that this method resets any previous predicates that may have been + * set, including headers excluded by default such as the RFC 9218 defined + * "Priority" header. + * @param headerPredicate the predicate to add + * @since 6.2.1 + */ + public void setHeaderPredicate(Predicate headerPredicate) { + this.headerPredicate = headerPredicate; + } + + + @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.headerSet()) { + String name = entry.getKey(); + if (!this.headerPredicate.test(entry.getKey())) { + continue; + } + List values = entry.getValue(); + if (!CollectionUtils.isEmpty(values)) { + // For constructor args with @BindParam mapped to the actual header name + addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values)); + // Also adapt to Java conventions for setters + name = StringUtils.uncapitalize(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/ExtendedWebExchangeDataBinder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java index 0c055d2a0ec..d5bd420797d 100644 --- 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 @@ -16,21 +16,9 @@ package org.springframework.web.reactive.result.method.annotation; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.function.Predicate; - import org.jspecify.annotations.Nullable; -import reactor.core.publisher.Mono; -import org.springframework.http.HttpHeaders; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; 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 @@ -41,93 +29,14 @@ import org.springframework.web.server.ServerWebExchange; * * @author Rossen Stoyanchev * @since 6.2.1 + * @deprecated in favor of the relocated + * {@link org.springframework.web.reactive.result.ExtendedWebExchangeDataBinder} */ -public class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder { - - private static final Set FILTERED_HEADER_NAMES = Set.of("accept", "authorization", "connection", - "cookie", "from", "host", "origin", "priority", "range", "referer", "upgrade"); - - - private Predicate headerPredicate = name -> !FILTERED_HEADER_NAMES.contains(name.toLowerCase(Locale.ROOT)); - - - /** - * Create a new instance, with default object name. - * @param target the target object to bind onto (or {@code null} if the - * binder is just used to convert a plain parameter value) - * @since 7.0.2 - * @see #DEFAULT_OBJECT_NAME - */ - public ExtendedWebExchangeDataBinder(@Nullable Object target) { - super(target); - } +@Deprecated(since = "7.0.2", forRemoval = true) +public class ExtendedWebExchangeDataBinder extends org.springframework.web.reactive.result.ExtendedWebExchangeDataBinder { public ExtendedWebExchangeDataBinder(@Nullable Object target, String objectName) { super(target, objectName); } - - /** - * Add a Predicate that filters the header names to use for data binding. - * Multiple predicates are combined with {@code AND}. - * @param headerPredicate the predicate to add - * @since 6.2.1 - */ - public void addHeaderPredicate(Predicate headerPredicate) { - this.headerPredicate = this.headerPredicate.and(headerPredicate); - } - - /** - * Set the Predicate that filters the header names to use for data binding. - *

Note that this method resets any previous predicates that may have been - * set, including headers excluded by default such as the RFC 9218 defined - * "Priority" header. - * @param headerPredicate the predicate to add - * @since 6.2.1 - */ - public void setHeaderPredicate(Predicate headerPredicate) { - this.headerPredicate = headerPredicate; - } - - - @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.headerSet()) { - String name = entry.getKey(); - if (!this.headerPredicate.test(entry.getKey())) { - continue; - } - List values = entry.getValue(); - if (!CollectionUtils.isEmpty(values)) { - // For constructor args with @BindParam mapped to the actual header name - addValueIfNotPresent(map, "Header", name, (values.size() == 1 ? values.get(0) : values)); - // Also adapt to Java conventions for setters - name = StringUtils.uncapitalize(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 96006e6b170..86ecd3fae13 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 @@ -72,12 +72,12 @@ class InitBinderBindingContext extends BindingContext { /** - * Returns an instance of {@link ExtendedWebExchangeDataBinder}. + * Returns an instance of {@link org.springframework.web.reactive.result.ExtendedWebExchangeDataBinder}. * @since 6.2.1 */ @Override protected WebExchangeDataBinder createBinderInstance(@Nullable Object target, String name) { - return new ExtendedWebExchangeDataBinder(target, name); + return new org.springframework.web.reactive.result.ExtendedWebExchangeDataBinder(target, name); } @Override 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 f738df63025..70f1f7c0382 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 @@ -215,7 +215,9 @@ class InitBinderBindingContextTests { MockServerWebExchange exchange = MockServerWebExchange.from(request); BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class); - ExtendedWebExchangeDataBinder binder = (ExtendedWebExchangeDataBinder) context.createDataBinder(exchange, null, "", null); + org.springframework.web.reactive.result.ExtendedWebExchangeDataBinder binder = + (org.springframework.web.reactive.result.ExtendedWebExchangeDataBinder) + context.createDataBinder(exchange, null, "", null); binder.addHeaderPredicate(name -> !name.equalsIgnoreCase("Another-Int-Array")); Map map = binder.getValuesToBind(exchange).block(); @@ -233,7 +235,9 @@ class InitBinderBindingContextTests { MockServerWebExchange exchange = MockServerWebExchange.from(request); BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class); - ExtendedWebExchangeDataBinder binder = (ExtendedWebExchangeDataBinder) context.createDataBinder(exchange, null, "", null); + org.springframework.web.reactive.result.ExtendedWebExchangeDataBinder binder = + (org.springframework.web.reactive.result.ExtendedWebExchangeDataBinder) + context.createDataBinder(exchange, null, "", null); Map map = binder.getValuesToBind(exchange).block(); assertThat(map).isEmpty(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java index 2cd368bed69..125f3061e15 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java @@ -76,7 +76,7 @@ import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.WebRequest; -import org.springframework.web.servlet.mvc.method.annotation.ExtendedServletRequestDataBinder; +import org.springframework.web.servlet.support.ExtendedServletRequestDataBinder; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.ServletRequestPathUtils; import org.springframework.web.util.UriBuilder; 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 0f42b4f38fc..336372a58be 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,22 +16,9 @@ package org.springframework.web.servlet.mvc.method.annotation; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.function.Predicate; - -import jakarta.servlet.ServletRequest; -import jakarta.servlet.http.HttpServletRequest; import org.jspecify.annotations.Nullable; -import org.springframework.beans.MutablePropertyValues; -import org.springframework.util.StringUtils; import org.springframework.web.bind.ServletRequestDataBinder; -import org.springframework.web.bind.WebDataBinder; import org.springframework.web.servlet.HandlerMapping; /** @@ -51,15 +38,11 @@ import org.springframework.web.servlet.HandlerMapping; * @since 3.1 * @see ServletRequestDataBinder * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE + * @deprecated in favor of the relocated + * {@link org.springframework.web.servlet.support.ExtendedServletRequestDataBinder} */ -public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder { - - private static final Set FILTERED_HEADER_NAMES = Set.of("accept", "authorization", "connection", - "cookie", "from", "host", "origin", "priority", "range", "referer", "upgrade"); - - - private Predicate headerPredicate = name -> !FILTERED_HEADER_NAMES.contains(name.toLowerCase(Locale.ROOT)); - +@Deprecated(since = "7.0.2", forRemoval = true) +public class ExtendedServletRequestDataBinder extends org.springframework.web.servlet.support.ExtendedServletRequestDataBinder { /** * Create a new instance, with default object name. @@ -82,143 +65,4 @@ public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder { super(target, objectName); } - - /** - * Add a Predicate that filters the header names to use for data binding. - * Multiple predicates are combined with {@code AND}. - * @param headerPredicate the predicate to add - * @since 6.2.1 - */ - public void addHeaderPredicate(Predicate headerPredicate) { - this.headerPredicate = this.headerPredicate.and(headerPredicate); - } - - /** - * Set the Predicate that filters the header names to use for data binding. - *

Note that this method resets any previous predicates that may have been - * set, including headers excluded by default such as the RFC 9218 defined - * "Priority" header. - * @param headerPredicate the predicate to add - * @since 6.2.1 - */ - public void setHeaderPredicate(Predicate headerPredicate) { - this.headerPredicate = headerPredicate; - } - - - @Override - protected ServletRequestValueResolver createValueResolver(ServletRequest request) { - return new ExtendedServletRequestValueResolver(request, this); - } - - /** - * Merge URI variables into the property values to use for data binding. - */ - @Override - protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { - Map uriVars = getUriVars(request); - 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 = normalizeHeaderName(name); - addValueIfNotPresent(mpvs, "Header", name, value); - } - } - } - } - - @SuppressWarnings("unchecked") - private static @Nullable Map getUriVars(ServletRequest request) { - return (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); - } - - 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); - } - } - - private @Nullable Object getHeaderValue(HttpServletRequest request, String name) { - if (!this.headerPredicate.test(name)) { - return null; - } - - 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; - } - - private static String normalizeHeaderName(String name) { - return StringUtils.uncapitalize(name.replace("-", "")); - } - - - /** - * Resolver of values that looks up URI path variables. - */ - private class ExtendedServletRequestValueResolver extends ServletRequestValueResolver { - - ExtendedServletRequestValueResolver(ServletRequest request, WebDataBinder dataBinder) { - super(request, dataBinder); - } - - @Override - protected @Nullable Object getRequestParameter(String name, Class type) { - Object value = super.getRequestParameter(name, type); - if (value == null) { - Map uriVars = getUriVars(getRequest()); - if (uriVars != null) { - value = uriVars.get(name); - } - if (value == null && getRequest() instanceof HttpServletRequest httpServletRequest) { - value = getHeaderValue(httpServletRequest, name); - } - } - return value; - } - - @Override - protected Set initParameterNames(ServletRequest request) { - Set set = super.initParameterNames(request); - Map uriVars = getUriVars(getRequest()); - if (uriVars != null) { - set.addAll(uriVars.keySet()); - } - if (request instanceof HttpServletRequest httpServletRequest) { - Enumeration enumeration = httpServletRequest.getHeaderNames(); - while (enumeration.hasMoreElements()) { - String name = enumeration.nextElement(); - if (headerPredicate.test(name)) { - set.add(normalizeHeaderName(name)); - } - } - } - return set; - } - } - } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestDataBinderFactory.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestDataBinderFactory.java index 60a83b63d00..72b79989d49 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestDataBinderFactory.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestDataBinderFactory.java @@ -46,13 +46,13 @@ public class ServletRequestDataBinderFactory extends InitBinderDataBinderFactory } /** - * Returns an instance of {@link ExtendedServletRequestDataBinder}. + * Returns an instance of {@link org.springframework.web.servlet.support.ExtendedServletRequestDataBinder}. */ @Override protected ServletRequestDataBinder createBinderInstance( @Nullable Object target, String objectName, NativeWebRequest request) throws Exception { - return new ExtendedServletRequestDataBinder(target, objectName); + return new org.springframework.web.servlet.support.ExtendedServletRequestDataBinder(target, objectName); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ExtendedServletRequestDataBinder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ExtendedServletRequestDataBinder.java new file mode 100644 index 00000000000..dc7ef312660 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ExtendedServletRequestDataBinder.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-present 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.servlet.support; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Subclass of {@link ServletRequestDataBinder} that adds URI template variables + * and request headers to the values used for data binding. + * + *

WARNING: Data binding can lead to security issues by exposing + * parts of the object graph that are not meant to be accessed or modified by + * external clients. Therefore, the design and use of data binding should be considered + * carefully with regard to security. For more details, please refer to the dedicated + * sections on data binding for + * Spring Web MVC and + * Spring WebFlux + * in the reference manual. + * + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @since 7.0.2 + * @see ServletRequestDataBinder + * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE + */ +public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder { + + private static final Set FILTERED_HEADER_NAMES = Set.of("accept", "authorization", "connection", + "cookie", "from", "host", "origin", "priority", "range", "referer", "upgrade"); + + + private Predicate headerPredicate = name -> !FILTERED_HEADER_NAMES.contains(name.toLowerCase(Locale.ROOT)); + + + /** + * Create a new instance, with default object name. + * @param target the target object to bind onto (or {@code null} + * if the binder is just used to convert a plain parameter value) + * @see #DEFAULT_OBJECT_NAME + */ + public ExtendedServletRequestDataBinder(@Nullable Object target) { + super(target); + } + + /** + * Create a new instance. + * @param target the target object to bind onto (or {@code null} + * if the binder is just used to convert a plain parameter value) + * @param objectName the name of the target object + * @see #DEFAULT_OBJECT_NAME + */ + public ExtendedServletRequestDataBinder(@Nullable Object target, String objectName) { + super(target, objectName); + } + + + /** + * Add a Predicate that filters the header names to use for data binding. + * Multiple predicates are combined with {@code AND}. + * @param headerPredicate the predicate to add + */ + public void addHeaderPredicate(Predicate headerPredicate) { + this.headerPredicate = this.headerPredicate.and(headerPredicate); + } + + /** + * Set the Predicate that filters the header names to use for data binding. + *

Note that this method resets any previous predicates that may have been + * set, including headers excluded by default such as the RFC 9218 defined + * "Priority" header. + * @param headerPredicate the predicate to add + */ + public void setHeaderPredicate(Predicate headerPredicate) { + this.headerPredicate = headerPredicate; + } + + + @Override + protected ServletRequestValueResolver createValueResolver(ServletRequest request) { + return new ExtendedServletRequestValueResolver(request, this); + } + + /** + * Merge URI variables into the property values to use for data binding. + */ + @Override + protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { + Map uriVars = getUriVars(request); + 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 = normalizeHeaderName(name); + addValueIfNotPresent(mpvs, "Header", name, value); + } + } + } + } + + @SuppressWarnings("unchecked") + private static @Nullable Map getUriVars(ServletRequest request) { + return (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + } + + 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); + } + } + + private @Nullable Object getHeaderValue(HttpServletRequest request, String name) { + if (!this.headerPredicate.test(name)) { + return null; + } + + 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; + } + + private static String normalizeHeaderName(String name) { + return StringUtils.uncapitalize(name.replace("-", "")); + } + + + /** + * Resolver of values that looks up URI path variables. + */ + private class ExtendedServletRequestValueResolver extends ServletRequestValueResolver { + + ExtendedServletRequestValueResolver(ServletRequest request, WebDataBinder dataBinder) { + super(request, dataBinder); + } + + @Override + protected @Nullable Object getRequestParameter(String name, Class type) { + Object value = super.getRequestParameter(name, type); + if (value == null) { + Map uriVars = getUriVars(getRequest()); + if (uriVars != null) { + value = uriVars.get(name); + } + if (value == null && getRequest() instanceof HttpServletRequest httpServletRequest) { + value = getHeaderValue(httpServletRequest, name); + } + } + return value; + } + + @Override + protected Set initParameterNames(ServletRequest request) { + Set set = super.initParameterNames(request); + Map uriVars = getUriVars(getRequest()); + if (uriVars != null) { + set.addAll(uriVars.keySet()); + } + if (request instanceof HttpServletRequest httpServletRequest) { + Enumeration enumeration = httpServletRequest.getHeaderNames(); + while (enumeration.hasMoreElements()) { + String name = enumeration.nextElement(); + if (headerPredicate.test(name)) { + set.add(normalizeHeaderName(name)); + } + } + } + return set; + } + } + +} 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/support/ExtendedServletRequestDataBinderTests.java similarity index 98% rename from spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinderTests.java rename to spring-webmvc/src/test/java/org/springframework/web/servlet/support/ExtendedServletRequestDataBinderTests.java index 1d06c0f4455..7e211ec8aaa 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/support/ExtendedServletRequestDataBinderTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.servlet.mvc.method.annotation; +package org.springframework.web.servlet.support; import java.util.Map;