Add WebExchangeDataBinder

Issue: SPR-14541
This commit is contained in:
Rossen Stoyanchev
2016-10-07 06:15:40 -04:00
parent 580b8b92f8
commit b28b3e8877
9 changed files with 454 additions and 10 deletions

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2002-2016 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
*
* http://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.bind;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import reactor.core.publisher.Mono;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ServerWebExchange;
/**
* Specialized {@link org.springframework.validation.DataBinder} to perform data
* binding from URL query params or form data in the request data to Java objects.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class WebExchangeDataBinder extends WebDataBinder {
private static final ResolvableType MULTIVALUE_MAP_TYPE = ResolvableType.forClass(MultiValueMap.class);
private HttpMessageReader<MultiValueMap<String, String>> formReader = null;
/**
* 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 WebExchangeDataBinder(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
*/
public WebExchangeDataBinder(Object target, String objectName) {
super(target, objectName);
}
public void setFormReader(HttpMessageReader<MultiValueMap<String, String>> formReader) {
this.formReader = formReader;
}
public HttpMessageReader<MultiValueMap<String, String>> getFormReader() {
return this.formReader;
}
/**
* Bind the URL query parameters or form data of the body of the given request
* to this binder's target. The request body is parsed if the content-type
* is "application/x-www-form-urlencoded".
*
* @param exchange the current exchange.
* @return a {@code Mono<Void>} to indicate the result
*/
public Mono<Void> bind(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
Mono<MultiValueMap<String, String>> queryParams = Mono.just(request.getQueryParams());
Mono<MultiValueMap<String, String>> formParams = getFormParams(exchange);
return Mono.zip(this::mergeParams, queryParams, formParams)
.map(this::getParamsToBind)
.doOnNext(values -> values.putAll(getMultipartFiles(exchange)))
.doOnNext(values -> values.putAll(getExtraValuesToBind(exchange)))
.then(values -> {
doBind(new MutablePropertyValues(values));
return Mono.empty();
});
}
private Mono<MultiValueMap<String, String>> getFormParams(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
MediaType contentType = request.getHeaders().getContentType();
if (this.formReader.canRead(MULTIVALUE_MAP_TYPE, contentType)) {
return this.formReader.readMono(MULTIVALUE_MAP_TYPE, request, Collections.emptyMap());
}
else {
return Mono.just(new LinkedMultiValueMap<>());
}
}
@SuppressWarnings("unchecked")
private MultiValueMap<String, String> mergeParams(Object[] paramMaps) {
MultiValueMap<String, String> result = new LinkedMultiValueMap<>();
Arrays.stream(paramMaps).forEach(map -> result.putAll((MultiValueMap<String, String>) map));
return result;
}
private Map<String, Object> getParamsToBind(MultiValueMap<String, String> params) {
Map<String, Object> valuesToBind = new TreeMap<>();
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
String name = entry.getKey();
List<String> values = entry.getValue();
if (values == null || values.isEmpty()) {
// Do nothing, no values found at all.
}
else {
if (values.size() > 1) {
valuesToBind.put(name, values);
}
else {
valuesToBind.put(name, values.get(0));
}
}
}
return valuesToBind;
}
/**
* Bind all multipart files contained in the given request, if any (in case
* of a multipart request).
* <p>Multipart files will only be added to the property values if they
* are not empty or if we're configured to bind empty multipart files too.
* @param exchange the current exchange
* @return Map of field name String to MultipartFile object
*/
protected Map<String, List<MultipartFile>> getMultipartFiles(ServerWebExchange exchange) {
// TODO
return Collections.emptyMap();
}
/**
* Extension point that subclasses can use to add extra bind values for a
* request. Invoked before {@link #doBind(MutablePropertyValues)}.
* The default implementation is empty.
* @param exchange the current exchange
*/
protected Map<String, ?> getExtraValuesToBind(ServerWebExchange exchange) {
return Collections.emptyMap();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2016 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.
@@ -22,7 +22,6 @@ import org.springframework.validation.BindingErrorProcessor;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.context.request.WebRequest;
/**
* Convenient {@link WebBindingInitializer} for declarative configuration
@@ -182,7 +181,7 @@ public class ConfigurableWebBindingInitializer implements WebBindingInitializer
@Override
public void initBinder(WebDataBinder binder, WebRequest request) {
public void initBinder(WebDataBinder binder) {
binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
if (this.directFieldAccess) {
binder.initDirectFieldAccess();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 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.
@@ -47,6 +47,7 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory {
* @throws Exception in case of invalid state or arguments
*/
@Override
@SuppressWarnings("deprecation")
public final WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName)
throws Exception {
@@ -74,7 +75,7 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory {
/**
* Extension point to further initialize the created data binder instance
* (e.g. with {@code @InitBinder} methods) after "global" initializaton
* (e.g. with {@code @InitBinder} methods) after "global" initialization
* via {@link WebBindingInitializer}.
* @param dataBinder the data binder instance to customize
* @param webRequest the current request

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2007 the original author or authors.
* Copyright 2002-2016 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.
@@ -20,19 +20,31 @@ import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.context.request.WebRequest;
/**
* Callback interface for initializing a {@link org.springframework.web.bind.WebDataBinder}
* for performing data binding in the context of a specific web request.
* Callback interface for initializing a {@link WebDataBinder} for performing
* data binding in the context of a specific web request.
*
* @author Juergen Hoeller
* @author Rossen Stoyanchev
* @since 2.5
*/
public interface WebBindingInitializer {
/**
* Initialize the given DataBinder for the given request.
* Initialize the given DataBinder.
* @param binder the DataBinder to initialize
* @since 5.0
*/
void initBinder(WebDataBinder binder);
/**
* Initialize the given DataBinder for the given (Servlet) request.
* @param binder the DataBinder to initialize
* @param request the web request that the data binding happens within
* @deprecated as of 5.0 in favor of {@link #initBinder(WebDataBinder)}
*/
void initBinder(WebDataBinder binder, WebRequest request);
@Deprecated
default void initBinder(WebDataBinder binder, WebRequest request) {
initBinder(binder);
}
}