Merge branch '6.2.x'

This commit is contained in:
rstoyanchev
2024-12-11 16:22:47 +00:00
7 changed files with 351 additions and 108 deletions

View File

@@ -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<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) {
return super.getValuesToBind(exchange).doOnNext(map -> {
Map<String, String> 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<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));
}
}
});
}
private static void addValueIfNotPresent(
Map<String, Object> 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}.
*/

View File

@@ -0,0 +1,120 @@
/*
* 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 java.util.Set;
import java.util.function.Predicate;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
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.
*
* <p>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 {
private static final Set<String> FILTERED_HEADER_NAMES = Set.of("Priority");
private Predicate<String> headerPredicate = name -> !FILTERED_HEADER_NAMES.contains(name);
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<String> headerPredicate) {
this.headerPredicate = this.headerPredicate.and(headerPredicate);
}
/**
* Set the Predicate that filters the header names to use for data binding.
* <p>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<String> headerPredicate) {
this.headerPredicate = headerPredicate;
}
@Override
public Mono<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) {
return super.getValuesToBind(exchange).doOnNext(map -> {
Map<String, String> 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<String, List<String>> entry : headers.entrySet()) {
String name = entry.getKey();
if (!this.headerPredicate.test(entry.getKey())) {
continue;
}
List<String> 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<String, Object> 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);
}
}
}
}

View File

@@ -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()