Merge reactive @ModelAttribute support
This commit is contained in:
commit
1f128110f7
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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.ui;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link Model} based on a {@link ConcurrentHashMap} for use
|
||||
* in concurrent scenarios. Exposed to handler methods by Spring Web Reactive
|
||||
* typically via a declaration of the {@link Model} interface. There is typically
|
||||
* no need to create it within user code. If necessary a controller method can
|
||||
* return a regular {@code java.util.Map}, or more likely a
|
||||
* {@code java.util.ConcurrentMap}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class ConcurrentModel extends ConcurrentHashMap<String, Object> implements Model {
|
||||
|
||||
/**
|
||||
* Construct a new, empty {@code ConcurrentModel}.
|
||||
*/
|
||||
public ConcurrentModel() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@code ModelMap} containing the supplied attribute
|
||||
* under the supplied name.
|
||||
* @see #addAttribute(String, Object)
|
||||
*/
|
||||
public ConcurrentModel(String attributeName, Object attributeValue) {
|
||||
addAttribute(attributeName, attributeValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@code ModelMap} containing the supplied attribute.
|
||||
* Uses attribute name generation to generate the key for the supplied model
|
||||
* object.
|
||||
* @see #addAttribute(Object)
|
||||
*/
|
||||
public ConcurrentModel(Object attributeValue) {
|
||||
addAttribute(attributeValue);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add the supplied attribute under the supplied name.
|
||||
* @param attributeName the name of the model attribute (never {@code null})
|
||||
* @param attributeValue the model attribute value (can be {@code null})
|
||||
*/
|
||||
public ConcurrentModel addAttribute(String attributeName, Object attributeValue) {
|
||||
Assert.notNull(attributeName, "Model attribute name must not be null");
|
||||
put(attributeName, attributeValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the supplied attribute to this {@code Map} using a
|
||||
* {@link org.springframework.core.Conventions#getVariableName generated name}.
|
||||
* <p><emphasis>Note: Empty {@link Collection Collections} are not added to
|
||||
* the model when using this method because we cannot correctly determine
|
||||
* the true convention name. View code should check for {@code null} rather
|
||||
* than for empty collections as is already done by JSTL tags.</emphasis>
|
||||
* @param attributeValue the model attribute value (never {@code null})
|
||||
*/
|
||||
public ConcurrentModel addAttribute(Object attributeValue) {
|
||||
Assert.notNull(attributeValue, "Model object must not be null");
|
||||
if (attributeValue instanceof Collection && ((Collection<?>) attributeValue).isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
return addAttribute(Conventions.getVariableName(attributeValue), attributeValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all attributes in the supplied {@code Collection} into this
|
||||
* {@code Map}, using attribute name generation for each element.
|
||||
* @see #addAttribute(Object)
|
||||
*/
|
||||
public ConcurrentModel addAllAttributes(Collection<?> attributeValues) {
|
||||
if (attributeValues != null) {
|
||||
for (Object attributeValue : attributeValues) {
|
||||
addAttribute(attributeValue);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all attributes in the supplied {@code Map} into this {@code Map}.
|
||||
* @see #addAttribute(String, Object)
|
||||
*/
|
||||
public ConcurrentModel addAllAttributes(Map<String, ?> attributes) {
|
||||
if (attributes != null) {
|
||||
putAll(attributes);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all attributes in the supplied {@code Map} into this {@code Map},
|
||||
* with existing objects of the same name taking precedence (i.e. not getting
|
||||
* replaced).
|
||||
*/
|
||||
public ConcurrentModel mergeAttributes(Map<String, ?> attributes) {
|
||||
if (attributes != null) {
|
||||
for (Map.Entry<String, ?> entry : attributes.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (!containsKey(key)) {
|
||||
put(key, entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this model contain an attribute of the given name?
|
||||
* @param attributeName the name of the model attribute (never {@code null})
|
||||
* @return whether this model contains a corresponding attribute
|
||||
*/
|
||||
public boolean containsAttribute(String attributeName) {
|
||||
return containsKey(attributeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> asMap() {
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2002-2015 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.validation.support;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.ui.ConcurrentModel;
|
||||
import org.springframework.validation.BindingResult;
|
||||
|
||||
/**
|
||||
* Sub-class of {@link ConcurrentModel} that automatically removes
|
||||
* the {@link BindingResult} object when its corresponding
|
||||
* target attribute is replaced through regular {@link Map} operations.
|
||||
*
|
||||
* <p>This is the class exposed to controller methods by Spring Web Reactive,
|
||||
* typically consumed through a declaration of the
|
||||
* {@link org.springframework.ui.Model} interface. There is typically
|
||||
* no need to create it within user code. If necessary a controller method can
|
||||
* return a regular {@code java.util.Map}, or more likely a
|
||||
* {@code java.util.ConcurrentMap}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
* @see BindingResult
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class BindingAwareConcurrentModel extends ConcurrentModel {
|
||||
|
||||
@Override
|
||||
public Object put(String key, Object value) {
|
||||
removeBindingResultIfNecessary(key, value);
|
||||
return super.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends String, ?> map) {
|
||||
map.entrySet().forEach(e -> removeBindingResultIfNecessary(e.getKey(), e.getValue()));
|
||||
super.putAll(map);
|
||||
}
|
||||
|
||||
private void removeBindingResultIfNecessary(String key, Object value) {
|
||||
if (!key.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
|
||||
String resultKey = BindingResult.MODEL_KEY_PREFIX + key;
|
||||
BindingResult result = (BindingResult) get(resultKey);
|
||||
if (result != null && result.getTarget() != value) {
|
||||
remove(resultKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -23,12 +23,12 @@ import reactor.core.publisher.Mono;
|
|||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.result.method.BindingContext;
|
||||
|
||||
/**
|
||||
* Represent the result of the invocation of a handler.
|
||||
* Represent the result of the invocation of a handler or a handler method.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
|
|
@ -37,12 +37,11 @@ public class HandlerResult {
|
|||
|
||||
private final Object handler;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private final Optional<Object> returnValue;
|
||||
private final Object returnValue;
|
||||
|
||||
private final ResolvableType returnType;
|
||||
|
||||
private final ModelMap model;
|
||||
private final BindingContext bindingContext;
|
||||
|
||||
private Function<Throwable, Mono<HandlerResult>> exceptionHandler;
|
||||
|
||||
|
|
@ -62,15 +61,17 @@ public class HandlerResult {
|
|||
* @param handler the handler that handled the request
|
||||
* @param returnValue the return value from the handler possibly {@code null}
|
||||
* @param returnType the return value type
|
||||
* @param model the model used for request handling
|
||||
* @param context the binding context used for request handling
|
||||
*/
|
||||
public HandlerResult(Object handler, Object returnValue, MethodParameter returnType, ModelMap model) {
|
||||
public HandlerResult(Object handler, Object returnValue, MethodParameter returnType,
|
||||
BindingContext context) {
|
||||
|
||||
Assert.notNull(handler, "'handler' is required");
|
||||
Assert.notNull(returnType, "'returnType' is required");
|
||||
this.handler = handler;
|
||||
this.returnValue = Optional.ofNullable(returnValue);
|
||||
this.returnValue = returnValue;
|
||||
this.returnType = ResolvableType.forMethodParameter(returnType);
|
||||
this.model = (model != null ? model : new ExtendedModelMap());
|
||||
this.bindingContext = (context != null ? context : new BindingContext());
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -85,7 +86,7 @@ public class HandlerResult {
|
|||
* Return the value returned from the handler wrapped as {@link Optional}.
|
||||
*/
|
||||
public Optional<Object> getReturnValue() {
|
||||
return this.returnValue;
|
||||
return Optional.ofNullable(this.returnValue);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -104,11 +105,18 @@ public class HandlerResult {
|
|||
}
|
||||
|
||||
/**
|
||||
* Return the model used during request handling with attributes that may be
|
||||
* used to render HTML templates with.
|
||||
* Return the BindingContext used for request handling.
|
||||
*/
|
||||
public ModelMap getModel() {
|
||||
return this.model;
|
||||
public BindingContext getBindingContext() {
|
||||
return this.bindingContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the model used for request handling. This is a shortcut for
|
||||
* {@code getBindingContext().getModel()}.
|
||||
*/
|
||||
public Model getModel() {
|
||||
return this.bindingContext.getModel();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,9 +15,8 @@
|
|||
*/
|
||||
package org.springframework.web.reactive.result.method;
|
||||
|
||||
import org.springframework.beans.TypeConverter;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.validation.support.BindingAwareModelMap;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.support.BindingAwareConcurrentModel;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.WebExchangeDataBinder;
|
||||
import org.springframework.web.bind.support.WebBindingInitializer;
|
||||
|
|
@ -33,12 +32,10 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
*/
|
||||
public class BindingContext {
|
||||
|
||||
private final ModelMap model = new BindingAwareModelMap();
|
||||
private final Model model = new BindingAwareConcurrentModel();
|
||||
|
||||
private final WebBindingInitializer initializer;
|
||||
|
||||
private final TypeConverter simpleValueTypeConverter;
|
||||
|
||||
|
||||
public BindingContext() {
|
||||
this(null);
|
||||
|
|
@ -46,7 +43,6 @@ public class BindingContext {
|
|||
|
||||
public BindingContext(WebBindingInitializer initializer) {
|
||||
this.initializer = initializer;
|
||||
this.simpleValueTypeConverter = initTypeConverter(initializer);
|
||||
}
|
||||
|
||||
private static WebExchangeDataBinder initTypeConverter(WebBindingInitializer initializer) {
|
||||
|
|
@ -61,7 +57,7 @@ public class BindingContext {
|
|||
/**
|
||||
* Return the default model.
|
||||
*/
|
||||
public ModelMap getModel() {
|
||||
public Model getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import org.springframework.core.DefaultParameterNameDiscoverer;
|
|||
import org.springframework.core.GenericTypeResolver;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
|
@ -101,9 +100,8 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||
return resolveArguments(exchange, bindingContext, providedArgs).then(args -> {
|
||||
try {
|
||||
Object value = doInvoke(args);
|
||||
ModelMap model = bindingContext.getModel();
|
||||
HandlerResult handlerResult = new HandlerResult(this, value, getReturnType(), model);
|
||||
return Mono.just(handlerResult);
|
||||
HandlerResult result = new HandlerResult(this, value, getReturnType(), bindingContext);
|
||||
return Mono.just(result);
|
||||
}
|
||||
catch (InvocationTargetException ex) {
|
||||
return Mono.error(ex.getTargetException());
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import org.springframework.web.reactive.result.method.BindingContext;
|
|||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
|
||||
import org.springframework.web.bind.WebExchangeBindException;
|
||||
|
||||
/**
|
||||
* Abstract base class for argument resolvers that resolve method arguments
|
||||
|
|
@ -216,7 +217,7 @@ public abstract class AbstractMessageReaderArgumentResolver {
|
|||
WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name);
|
||||
binder.validate(validationHints);
|
||||
if (binder.getBindingResult().hasErrors()) {
|
||||
throw new ServerWebInputException("Validation failed", param);
|
||||
throw new WebExchangeBindException(param, binder.getBindingResult());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import org.springframework.beans.factory.config.BeanExpressionContext;
|
|||
import org.springframework.beans.factory.config.BeanExpressionResolver;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.ValueConstants;
|
||||
import org.springframework.web.reactive.result.method.BindingContext;
|
||||
|
|
@ -87,7 +87,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
|
|||
"Specified name must not resolve to null: [" + namedValueInfo.name + "]"));
|
||||
}
|
||||
|
||||
ModelMap model = bindingContext.getModel();
|
||||
Model model = bindingContext.getModel();
|
||||
|
||||
return resolveName(resolvedName.toString(), nestedParameter, exchange)
|
||||
.map(arg -> {
|
||||
|
|
@ -186,7 +186,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
|
|||
}
|
||||
|
||||
private Mono<Object> getDefaultValue(NamedValueInfo namedValueInfo, MethodParameter parameter,
|
||||
BindingContext bindingContext, ModelMap model, ServerWebExchange exchange) {
|
||||
BindingContext bindingContext, Model model, ServerWebExchange exchange) {
|
||||
|
||||
Object value = null;
|
||||
try {
|
||||
|
|
@ -263,7 +263,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
|
|||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
protected void handleResolvedValue(Object arg, String name, MethodParameter parameter,
|
||||
ModelMap model, ServerWebExchange exchange) {
|
||||
Model model, ServerWebExchange exchange) {
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* 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.reactive.result.method.annotation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ReactiveAdapter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.support.WebBindingInitializer;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.result.method.BindingContext;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.reactive.result.method.InvocableHandlerMethod;
|
||||
import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
|
||||
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* A helper class for {@link RequestMappingHandlerAdapter} that assists with
|
||||
* creating a {@code BindingContext} and initialize it, and its model, through
|
||||
* {@code @InitBinder} and {@code @ModelAttribute} methods.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
class BindingContextFactory {
|
||||
|
||||
private final RequestMappingHandlerAdapter adapter;
|
||||
|
||||
|
||||
public BindingContextFactory(RequestMappingHandlerAdapter adapter) {
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
|
||||
public RequestMappingHandlerAdapter getAdapter() {
|
||||
return this.adapter;
|
||||
}
|
||||
|
||||
private WebBindingInitializer getBindingInitializer() {
|
||||
return getAdapter().getWebBindingInitializer();
|
||||
}
|
||||
|
||||
private List<SyncHandlerMethodArgumentResolver> getInitBinderArgumentResolvers() {
|
||||
return getAdapter().getInitBinderArgumentResolvers();
|
||||
}
|
||||
|
||||
private List<HandlerMethodArgumentResolver> getArgumentResolvers() {
|
||||
return getAdapter().getArgumentResolvers();
|
||||
}
|
||||
|
||||
private ReactiveAdapterRegistry getAdapterRegistry() {
|
||||
return getAdapter().getReactiveAdapterRegistry();
|
||||
}
|
||||
|
||||
private Stream<Method> getInitBinderMethods(HandlerMethod handlerMethod) {
|
||||
return getAdapter().getInitBinderMethods(handlerMethod.getBeanType()).stream();
|
||||
}
|
||||
|
||||
private Stream<Method> getModelAttributeMethods(HandlerMethod handlerMethod) {
|
||||
return getAdapter().getModelAttributeMethods(handlerMethod.getBeanType()).stream();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create and initialize a BindingContext for the current request.
|
||||
* @param handlerMethod the request handling method
|
||||
* @param exchange the current exchange
|
||||
* @return Mono with the BindingContext instance
|
||||
*/
|
||||
public Mono<BindingContext> createBindingContext(HandlerMethod handlerMethod,
|
||||
ServerWebExchange exchange) {
|
||||
|
||||
List<SyncInvocableHandlerMethod> invocableMethods = getInitBinderMethods(handlerMethod)
|
||||
.map(method -> {
|
||||
Object bean = handlerMethod.getBean();
|
||||
SyncInvocableHandlerMethod invocable = new SyncInvocableHandlerMethod(bean, method);
|
||||
invocable.setSyncArgumentResolvers(getInitBinderArgumentResolvers());
|
||||
return invocable;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
BindingContext bindingContext =
|
||||
new InitBinderBindingContext(getBindingInitializer(), invocableMethods);
|
||||
|
||||
return initModel(handlerMethod, bindingContext, exchange).then(Mono.just(bindingContext));
|
||||
}
|
||||
|
||||
@SuppressWarnings("Convert2MethodRef")
|
||||
private Mono<Void> initModel(HandlerMethod handlerMethod, BindingContext context,
|
||||
ServerWebExchange exchange) {
|
||||
|
||||
List<Mono<HandlerResult>> resultMonos = getModelAttributeMethods(handlerMethod)
|
||||
.map(method -> {
|
||||
Object bean = handlerMethod.getBean();
|
||||
InvocableHandlerMethod invocable = new InvocableHandlerMethod(bean, method);
|
||||
invocable.setArgumentResolvers(getArgumentResolvers());
|
||||
return invocable;
|
||||
})
|
||||
.map(invocable -> invocable.invoke(exchange, context))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Mono
|
||||
.when(resultMonos, resultArr -> processModelMethodMonos(resultArr, context))
|
||||
.then(voidMonos -> Mono.when(voidMonos));
|
||||
}
|
||||
|
||||
private List<Mono<Void>> processModelMethodMonos(Object[] resultArr, BindingContext context) {
|
||||
return Arrays.stream(resultArr)
|
||||
.map(result -> processModelMethodResult((HandlerResult) result, context))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Mono<Void> processModelMethodResult(HandlerResult result, BindingContext context) {
|
||||
Object value = result.getReturnValue().orElse(null);
|
||||
if (value == null) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
ResolvableType type = result.getReturnType();
|
||||
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(type.getRawClass(), value);
|
||||
Class<?> valueType = (adapter != null ? type.resolveGeneric(0) : type.resolve());
|
||||
|
||||
if (Void.class.equals(valueType) || void.class.equals(valueType)) {
|
||||
return (adapter != null ? adapter.toMono(value) : Mono.empty());
|
||||
}
|
||||
|
||||
String name = getAttributeName(valueType, result.getReturnTypeSource());
|
||||
context.getModel().asMap().putIfAbsent(name, value);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
private String getAttributeName(Class<?> valueType, MethodParameter parameter) {
|
||||
Method method = parameter.getMethod();
|
||||
ModelAttribute annot = AnnotatedElementUtils.findMergedAnnotation(method, ModelAttribute.class);
|
||||
if (annot != null && StringUtils.hasText(annot.value())) {
|
||||
return annot.value();
|
||||
}
|
||||
// TODO: Conventions does not deal with async wrappers
|
||||
return ClassUtils.getShortNameAsProperty(valueType);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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.reactive.result.method.annotation;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ReactiveAdapter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.reactive.result.method.BindingContext;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Resolve {@link Errors} or {@link BindingResult} method arguments.
|
||||
* An {@code Errors} argument is expected to appear immediately after the
|
||||
* model attribute in the method signature.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ErrorsMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final ReactiveAdapterRegistry adapterRegistry;
|
||||
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
* @param registry for adapting to other reactive types from and to Mono
|
||||
*/
|
||||
public ErrorsMethodArgumentResolver(ReactiveAdapterRegistry registry) {
|
||||
Assert.notNull(registry, "'ReactiveAdapterRegistry' is required.");
|
||||
this.adapterRegistry = registry;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured {@link ReactiveAdapterRegistry}.
|
||||
*/
|
||||
public ReactiveAdapterRegistry getAdapterRegistry() {
|
||||
return this.adapterRegistry;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
Class<?> clazz = parameter.getParameterType();
|
||||
return Errors.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext context,
|
||||
ServerWebExchange exchange) {
|
||||
|
||||
String name = getModelAttributeName(parameter);
|
||||
Object errors = context.getModel().asMap().get(BindingResult.MODEL_KEY_PREFIX + name);
|
||||
|
||||
Mono<?> errorsMono;
|
||||
if (Mono.class.isAssignableFrom(errors.getClass())) {
|
||||
errorsMono = (Mono<?>) errors;
|
||||
}
|
||||
else if (Errors.class.isAssignableFrom(errors.getClass())) {
|
||||
errorsMono = Mono.just(errors);
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(
|
||||
"Unexpected Errors/BindingResult type: " + errors.getClass().getName());
|
||||
}
|
||||
|
||||
return errorsMono.cast(Object.class);
|
||||
}
|
||||
|
||||
private String getModelAttributeName(MethodParameter parameter) {
|
||||
|
||||
Assert.isTrue(parameter.getParameterIndex() > 0,
|
||||
"Errors argument must be immediately after a model attribute argument.");
|
||||
|
||||
int index = parameter.getParameterIndex() - 1;
|
||||
MethodParameter attributeParam = new MethodParameter(parameter.getMethod(), index);
|
||||
Class<?> attributeType = attributeParam.getParameterType();
|
||||
|
||||
ResolvableType type = ResolvableType.forMethodParameter(attributeParam);
|
||||
ReactiveAdapter adapterTo = getAdapterRegistry().getAdapterTo(type.resolve());
|
||||
|
||||
Assert.isNull(adapterTo, "Errors/BindingResult cannot be used with an async model attribute. " +
|
||||
"Either declare the model attribute without the async wrapper type " +
|
||||
"or handle WebExchangeBindException through the async type.");
|
||||
|
||||
ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class);
|
||||
if (annot != null && StringUtils.hasText(annot.value())) {
|
||||
return annot.value();
|
||||
}
|
||||
// TODO: Conventions does not deal with async wrappers
|
||||
return ClassUtils.getShortNameAsProperty(attributeType);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* 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.reactive.result.method.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoProcessor;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ReactiveAdapter;
|
||||
import org.springframework.core.ReactiveAdapter.Descriptor;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.WebExchangeBindException;
|
||||
import org.springframework.web.bind.WebExchangeDataBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.reactive.result.method.BindingContext;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Resolve {@code @ModelAttribute} annotated method arguments.
|
||||
*
|
||||
* <p>Model attributes are sourced from the model, or created using a default
|
||||
* constructor and then added to the model. Once created the attribute is
|
||||
* populated via data binding to the request (form data, query params).
|
||||
* Validation also may be applied if the argument is annotated with
|
||||
* {@code @javax.validation.Valid} or Spring's own
|
||||
* {@code @org.springframework.validation.annotation.Validated}.
|
||||
*
|
||||
* <p>When this handler is created with {@code useDefaultResolution=true}
|
||||
* any non-simple type argument and return value is regarded as a model
|
||||
* attribute with or without the presence of an {@code @ModelAttribute}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ModelAttributeMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final ReactiveAdapterRegistry adapterRegistry;
|
||||
|
||||
private final boolean useDefaultResolution;
|
||||
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
* @param registry for adapting to other reactive types from and to Mono
|
||||
*/
|
||||
public ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry registry) {
|
||||
this(registry, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor with a default resolution mode flag.
|
||||
* @param registry for adapting to other reactive types from and to Mono
|
||||
* @param useDefaultResolution if "true", non-simple method arguments and
|
||||
* return values are considered model attributes with or without a
|
||||
* {@code @ModelAttribute} annotation present.
|
||||
*/
|
||||
public ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry registry,
|
||||
boolean useDefaultResolution) {
|
||||
|
||||
Assert.notNull(registry, "'ReactiveAdapterRegistry' is required.");
|
||||
this.useDefaultResolution = useDefaultResolution;
|
||||
this.adapterRegistry = registry;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured {@link ReactiveAdapterRegistry}.
|
||||
*/
|
||||
public ReactiveAdapterRegistry getAdapterRegistry() {
|
||||
return this.adapterRegistry;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
|
||||
return true;
|
||||
}
|
||||
if (this.useDefaultResolution) {
|
||||
Class<?> clazz = parameter.getParameterType();
|
||||
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(clazz);
|
||||
if (adapter != null) {
|
||||
Descriptor descriptor = adapter.getDescriptor();
|
||||
if (descriptor.isNoValue() || descriptor.isMultiValue()) {
|
||||
return false;
|
||||
}
|
||||
clazz = ResolvableType.forMethodParameter(parameter).getGeneric(0).getRawClass();
|
||||
}
|
||||
return !BeanUtils.isSimpleProperty(clazz);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext context,
|
||||
ServerWebExchange exchange) {
|
||||
|
||||
ResolvableType type = ResolvableType.forMethodParameter(parameter);
|
||||
ReactiveAdapter adapterTo = getAdapterRegistry().getAdapterTo(type.resolve());
|
||||
Class<?> valueType = (adapterTo != null ? type.resolveGeneric(0) : parameter.getParameterType());
|
||||
String name = getAttributeName(valueType, parameter);
|
||||
Mono<?> valueMono = getAttributeMono(name, valueType, parameter, context, exchange);
|
||||
|
||||
Map<String, Object> model = context.getModel().asMap();
|
||||
MonoProcessor<BindingResult> bindingResultMono = MonoProcessor.create();
|
||||
model.put(BindingResult.MODEL_KEY_PREFIX + name, bindingResultMono);
|
||||
|
||||
return valueMono.then(value -> {
|
||||
WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name);
|
||||
return binder.bind(exchange)
|
||||
.doOnError(bindingResultMono::onError)
|
||||
.doOnSuccess(aVoid -> {
|
||||
validateIfApplicable(binder, parameter);
|
||||
BindingResult errors = binder.getBindingResult();
|
||||
model.put(BindingResult.MODEL_KEY_PREFIX + name, errors);
|
||||
model.put(name, value);
|
||||
bindingResultMono.onNext(errors);
|
||||
})
|
||||
.then(Mono.fromCallable(() -> {
|
||||
BindingResult errors = binder.getBindingResult();
|
||||
if (adapterTo != null) {
|
||||
return adapterTo.fromPublisher(errors.hasErrors() ?
|
||||
Mono.error(new WebExchangeBindException(parameter, errors)) :
|
||||
Mono.just(value));
|
||||
}
|
||||
else {
|
||||
if (errors.hasErrors() && checkErrorsArgument(parameter)) {
|
||||
throw new WebExchangeBindException(parameter, errors);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private String getAttributeName(Class<?> valueType, MethodParameter parameter) {
|
||||
ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class);
|
||||
if (annot != null && StringUtils.hasText(annot.value())) {
|
||||
return annot.value();
|
||||
}
|
||||
// TODO: Conventions does not deal with async wrappers
|
||||
return ClassUtils.getShortNameAsProperty(valueType);
|
||||
}
|
||||
|
||||
private Mono<?> getAttributeMono(String attributeName, Class<?> attributeType,
|
||||
MethodParameter param, BindingContext context, ServerWebExchange exchange) {
|
||||
|
||||
Object attribute = context.getModel().asMap().get(attributeName);
|
||||
if (attribute == null) {
|
||||
attribute = createAttribute(attributeName, attributeType, param, context, exchange);
|
||||
}
|
||||
if (attribute != null) {
|
||||
ReactiveAdapter adapterFrom = getAdapterRegistry().getAdapterFrom(null, attribute);
|
||||
if (adapterFrom != null) {
|
||||
return adapterFrom.toMono(attribute);
|
||||
}
|
||||
}
|
||||
return Mono.justOrEmpty(attribute);
|
||||
}
|
||||
|
||||
protected Object createAttribute(String attributeName, Class<?> attributeType,
|
||||
MethodParameter parameter, BindingContext context, ServerWebExchange exchange) {
|
||||
|
||||
return BeanUtils.instantiateClass(attributeType);
|
||||
}
|
||||
|
||||
protected boolean checkErrorsArgument(MethodParameter methodParam) {
|
||||
int i = methodParam.getParameterIndex();
|
||||
Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
|
||||
return paramTypes.length <= (i + 1) || !Errors.class.isAssignableFrom(paramTypes[i + 1]);
|
||||
}
|
||||
|
||||
protected void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) {
|
||||
Annotation[] annotations = parameter.getParameterAnnotations();
|
||||
for (Annotation ann : annotations) {
|
||||
Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class);
|
||||
if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
|
||||
Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann));
|
||||
Object hintArray = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
|
||||
binder.validate(hintArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import java.util.Optional;
|
|||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.ValueConstants;
|
||||
|
|
@ -93,7 +93,7 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueSyncAr
|
|||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void handleResolvedValue(Object arg, String name, MethodParameter parameter,
|
||||
ModelMap model, ServerWebExchange exchange) {
|
||||
Model model, ServerWebExchange exchange) {
|
||||
|
||||
// TODO: View.PATH_VARIABLES ?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ import org.springframework.http.codec.DecoderHttpMessageReader;
|
|||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.support.WebBindingInitializer;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
|
||||
|
|
@ -51,7 +53,6 @@ import org.springframework.web.reactive.result.method.BindingContext;
|
|||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.reactive.result.method.InvocableHandlerMethod;
|
||||
import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
|
||||
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
|
|
@ -82,8 +83,12 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
|
|||
private ConfigurableBeanFactory beanFactory;
|
||||
|
||||
|
||||
private final BindingContextFactory bindingContextFactory = new BindingContextFactory(this);
|
||||
|
||||
private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);
|
||||
|
||||
private final Map<Class<?>, Set<Method>> modelAttributeCache = new ConcurrentHashMap<>(64);
|
||||
|
||||
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
|
||||
new ConcurrentHashMap<>(64);
|
||||
|
||||
|
|
@ -225,11 +230,12 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
|
|||
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
|
||||
|
||||
// Annotation-based argument resolution
|
||||
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
|
||||
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory()));
|
||||
resolvers.add(new RequestParamMapMethodArgumentResolver());
|
||||
resolvers.add(new PathVariableMethodArgumentResolver(getBeanFactory()));
|
||||
resolvers.add(new PathVariableMapMethodArgumentResolver());
|
||||
resolvers.add(new RequestBodyArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry()));
|
||||
resolvers.add(new ModelAttributeMethodArgumentResolver(getReactiveAdapterRegistry()));
|
||||
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
|
||||
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
|
||||
resolvers.add(new CookieValueMethodArgumentResolver(getBeanFactory()));
|
||||
|
|
@ -240,6 +246,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
|
|||
// Type-based argument resolution
|
||||
resolvers.add(new HttpEntityArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry()));
|
||||
resolvers.add(new ModelArgumentResolver());
|
||||
resolvers.add(new ErrorsMethodArgumentResolver(getReactiveAdapterRegistry()));
|
||||
resolvers.add(new ServerWebExchangeArgumentResolver());
|
||||
resolvers.add(new PrincipalArgumentResolver());
|
||||
resolvers.add(new WebSessionArgumentResolver());
|
||||
|
|
@ -251,6 +258,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
|
|||
|
||||
// Catch-all
|
||||
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
|
||||
resolvers.add(new ModelAttributeMethodArgumentResolver(getReactiveAdapterRegistry(), true));
|
||||
return resolvers;
|
||||
}
|
||||
|
||||
|
|
@ -290,34 +298,31 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
|
|||
|
||||
@Override
|
||||
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
|
||||
|
||||
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
|
||||
invocable.setArgumentResolvers(getArgumentResolvers());
|
||||
BindingContext bindingContext = getBindingContext(handlerMethod);
|
||||
return invocable.invoke(exchange, bindingContext)
|
||||
.map(result -> result.setExceptionHandler(
|
||||
ex -> handleException(ex, handlerMethod, bindingContext, exchange)))
|
||||
.otherwise(ex -> handleException(
|
||||
ex, handlerMethod, bindingContext, exchange));
|
||||
|
||||
Mono<BindingContext> bindingContextMono =
|
||||
this.bindingContextFactory.createBindingContext(handlerMethod, exchange);
|
||||
|
||||
return bindingContextMono.then(bindingContext ->
|
||||
invocable.invoke(exchange, bindingContext)
|
||||
.doOnNext(result -> result.setExceptionHandler(
|
||||
ex -> handleException(ex, handlerMethod, bindingContext, exchange)))
|
||||
.otherwise(ex -> handleException(
|
||||
ex, handlerMethod, bindingContext, exchange)));
|
||||
}
|
||||
|
||||
private BindingContext getBindingContext(HandlerMethod handlerMethod) {
|
||||
Class<?> handlerType = handlerMethod.getBeanType();
|
||||
Set<Method> methods = this.initBinderCache.get(handlerType);
|
||||
if (methods == null) {
|
||||
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
|
||||
this.initBinderCache.put(handlerType, methods);
|
||||
}
|
||||
List<SyncInvocableHandlerMethod> initBinderMethods = new ArrayList<>();
|
||||
for (Method method : methods) {
|
||||
Object bean = handlerMethod.getBean();
|
||||
SyncInvocableHandlerMethod initBinderMethod = new SyncInvocableHandlerMethod(bean, method);
|
||||
initBinderMethod.setSyncArgumentResolvers(getInitBinderArgumentResolvers());
|
||||
initBinderMethods.add(initBinderMethod);
|
||||
}
|
||||
return new InitBinderBindingContext(getWebBindingInitializer(), initBinderMethods);
|
||||
Set<Method> getInitBinderMethods(Class<?> handlerType) {
|
||||
return this.initBinderCache.computeIfAbsent(handlerType, aClass ->
|
||||
MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS));
|
||||
}
|
||||
|
||||
Set<Method> getModelAttributeMethods(Class<?> handlerType) {
|
||||
return this.modelAttributeCache.computeIfAbsent(handlerType, aClass ->
|
||||
MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS));
|
||||
}
|
||||
|
||||
private Mono<HandlerResult> handleException(Throwable ex, HandlerMethod handlerMethod,
|
||||
BindingContext bindingContext, ServerWebExchange exchange) {
|
||||
|
|
@ -329,7 +334,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
|
|||
logger.debug("Invoking @ExceptionHandler method: " + invocable.getMethod());
|
||||
}
|
||||
invocable.setArgumentResolvers(getArgumentResolvers());
|
||||
bindingContext.getModel().clear();
|
||||
bindingContext.getModel().asMap().clear();
|
||||
return invocable.invoke(exchange, bindingContext, ex);
|
||||
}
|
||||
catch (Throwable invocationEx) {
|
||||
|
|
@ -360,7 +365,14 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
|
|||
/**
|
||||
* MethodFilter that matches {@link InitBinder @InitBinder} methods.
|
||||
*/
|
||||
public static final ReflectionUtils.MethodFilter INIT_BINDER_METHODS =
|
||||
method -> AnnotationUtils.findAnnotation(method, InitBinder.class) != null;
|
||||
public static final ReflectionUtils.MethodFilter INIT_BINDER_METHODS = method ->
|
||||
AnnotationUtils.findAnnotation(method, InitBinder.class) != null;
|
||||
|
||||
/**
|
||||
* MethodFilter that matches {@link ModelAttribute @ModelAttribute} methods.
|
||||
*/
|
||||
public static final ReflectionUtils.MethodFilter MODEL_ATTRIBUTE_METHODS = method ->
|
||||
(AnnotationUtils.findAnnotation(method, RequestMapping.class) == null) &&
|
||||
(AnnotationUtils.findAnnotation(method, ModelAttribute.class) != null);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
|
|
@ -57,6 +55,17 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueSyncAr
|
|||
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
* @param beanFactory a bean factory used for resolving ${...} placeholder
|
||||
* and #{...} SpEL expressions in default values, or {@code null} if default
|
||||
* values are not expected to contain expressions
|
||||
*/
|
||||
public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory) {
|
||||
this(beanFactory, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor with a default resolution mode flag.
|
||||
* @param beanFactory a bean factory used for resolving ${...} placeholder
|
||||
* and #{...} SpEL expressions in default values, or {@code null} if default
|
||||
* values are not expected to contain expressions
|
||||
|
|
@ -65,7 +74,9 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueSyncAr
|
|||
* is treated as a request parameter even if it isn't annotated, the
|
||||
* request parameter name is derived from the method parameter name.
|
||||
*/
|
||||
public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) {
|
||||
public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
|
||||
boolean useDefaultResolution) {
|
||||
|
||||
super(beanFactory);
|
||||
this.useDefaultResolution = useDefaultResolution;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,20 +16,19 @@
|
|||
|
||||
package org.springframework.web.reactive.result.view;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.core.GenericTypeResolver;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.ReactiveAdapter;
|
||||
|
|
@ -38,12 +37,16 @@ import org.springframework.core.ResolvableType;
|
|||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.WebExchangeDataBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.HandlerResultHandler;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
import org.springframework.web.reactive.result.AbstractHandlerResultHandler;
|
||||
import org.springframework.web.reactive.result.method.BindingContext;
|
||||
import org.springframework.web.server.NotAcceptableStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.HttpRequestPathHelper;
|
||||
|
|
@ -77,6 +80,11 @@ import org.springframework.web.util.HttpRequestPathHelper;
|
|||
public class ViewResolutionResultHandler extends AbstractHandlerResultHandler
|
||||
implements HandlerResultHandler, Ordered {
|
||||
|
||||
private static final Object NO_VALUE = new Object();
|
||||
|
||||
private static final Mono<Object> NO_VALUE_MONO = Mono.just(NO_VALUE);
|
||||
|
||||
|
||||
private final List<ViewResolver> viewResolvers = new ArrayList<>(4);
|
||||
|
||||
private final List<View> defaultViews = new ArrayList<>(4);
|
||||
|
|
@ -172,89 +180,78 @@ public class ViewResolutionResultHandler extends AbstractHandlerResultHandler
|
|||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
|
||||
|
||||
Mono<Object> valueMono;
|
||||
Mono<Object> returnValueMono;
|
||||
ResolvableType elementType;
|
||||
ResolvableType returnType = result.getReturnType();
|
||||
ResolvableType parameterType = result.getReturnType();
|
||||
|
||||
Optional<Object> optional = result.getReturnValue();
|
||||
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(returnType.getRawClass(), optional);
|
||||
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(parameterType.getRawClass(), optional);
|
||||
|
||||
if (adapter != null) {
|
||||
if (optional.isPresent()) {
|
||||
Mono<?> converted = adapter.toMono(optional);
|
||||
valueMono = converted.map(o -> o);
|
||||
}
|
||||
else {
|
||||
valueMono = Mono.empty();
|
||||
}
|
||||
elementType = adapter.getDescriptor().isNoValue() ?
|
||||
ResolvableType.forClass(Void.class) : returnType.getGeneric(0);
|
||||
returnValueMono = optional
|
||||
.map(value -> adapter.toMono(value).cast(Object.class))
|
||||
.orElse(Mono.empty());
|
||||
elementType = !adapter.getDescriptor().isNoValue() ?
|
||||
parameterType.getGeneric(0) : ResolvableType.forClass(Void.class);
|
||||
}
|
||||
else {
|
||||
valueMono = Mono.justOrEmpty(result.getReturnValue());
|
||||
elementType = returnType;
|
||||
returnValueMono = Mono.justOrEmpty(result.getReturnValue());
|
||||
elementType = parameterType;
|
||||
}
|
||||
|
||||
Mono<Object> viewMono;
|
||||
if (isViewNameOrReference(elementType, result)) {
|
||||
Mono<Object> viewName = getDefaultViewNameMono(exchange, result);
|
||||
viewMono = valueMono.otherwiseIfEmpty(viewName);
|
||||
}
|
||||
else {
|
||||
viewMono = valueMono.map(value -> updateModel(value, result))
|
||||
.defaultIfEmpty(result.getModel())
|
||||
.then(model -> getDefaultViewNameMono(exchange, result));
|
||||
}
|
||||
Map<String, ?> model = result.getModel();
|
||||
return viewMono.then(view -> {
|
||||
updateResponseStatus(result.getReturnTypeSource(), exchange);
|
||||
if (view instanceof View) {
|
||||
return ((View) view).render(model, null, exchange);
|
||||
}
|
||||
else if (view instanceof CharSequence) {
|
||||
String viewName = view.toString();
|
||||
Locale locale = Locale.getDefault(); // TODO
|
||||
return resolveAndRender(viewName, locale, model, exchange);
|
||||
return returnValueMono
|
||||
.otherwiseIfEmpty(exchange.isNotModified() ? Mono.empty() : NO_VALUE_MONO)
|
||||
.then(returnValue -> {
|
||||
|
||||
}
|
||||
else {
|
||||
// Should not happen
|
||||
return Mono.error(new IllegalStateException("Unexpected view type"));
|
||||
}
|
||||
});
|
||||
}
|
||||
updateResponseStatus(result.getReturnTypeSource(), exchange);
|
||||
|
||||
private boolean isViewNameOrReference(ResolvableType elementType, HandlerResult result) {
|
||||
Class<?> clazz = elementType.getRawClass();
|
||||
return (View.class.isAssignableFrom(clazz) ||
|
||||
(CharSequence.class.isAssignableFrom(clazz) && !hasModelAttributeAnnotation(result)));
|
||||
}
|
||||
Mono<List<View>> viewsMono;
|
||||
Model model = result.getModel();
|
||||
Locale locale = Locale.getDefault(); // TODO
|
||||
|
||||
private Mono<Object> getDefaultViewNameMono(ServerWebExchange exchange, HandlerResult result) {
|
||||
if (exchange.isNotModified()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
String defaultViewName = getDefaultViewName(result, exchange);
|
||||
if (defaultViewName != null) {
|
||||
return Mono.just(defaultViewName);
|
||||
}
|
||||
else {
|
||||
return Mono.error(new IllegalStateException(
|
||||
"Handler [" + result.getHandler() + "] " +
|
||||
"neither returned a view name nor a View object"));
|
||||
}
|
||||
Class<?> clazz = elementType.getRawClass();
|
||||
if (clazz == null) {
|
||||
clazz = returnValue.getClass();
|
||||
}
|
||||
|
||||
if (returnValue == NO_VALUE || Void.class.equals(clazz) || void.class.equals(clazz)) {
|
||||
viewsMono = resolveViews(getDefaultViewName(result, exchange), locale);
|
||||
}
|
||||
else if (Model.class.isAssignableFrom(clazz)) {
|
||||
model.addAllAttributes(((Model) returnValue).asMap());
|
||||
viewsMono = resolveViews(getDefaultViewName(result, exchange), locale);
|
||||
}
|
||||
else if (Map.class.isAssignableFrom(clazz)) {
|
||||
model.addAllAttributes((Map<String, ?>) returnValue);
|
||||
viewsMono = resolveViews(getDefaultViewName(result, exchange), locale);
|
||||
}
|
||||
else if (View.class.isAssignableFrom(clazz)) {
|
||||
viewsMono = Mono.just(Collections.singletonList((View) returnValue));
|
||||
}
|
||||
else if (CharSequence.class.isAssignableFrom(clazz) && !hasModelAttributeAnnotation(result)) {
|
||||
viewsMono = resolveViews(returnValue.toString(), locale);
|
||||
}
|
||||
else {
|
||||
String name = getNameForReturnValue(clazz, result.getReturnTypeSource());
|
||||
model.addAttribute(name, returnValue);
|
||||
viewsMono = resolveViews(getDefaultViewName(result, exchange), locale);
|
||||
}
|
||||
|
||||
return resolveAsyncAttributes(model.asMap())
|
||||
.doOnSuccess(aVoid -> addBindingResult(result, exchange))
|
||||
.then(viewsMono)
|
||||
.then(views -> render(views, model.asMap(), exchange));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the given request into a default view name. This is useful when
|
||||
* the application leaves the view name unspecified.
|
||||
* <p>The default implementation strips the leading and trailing slash from
|
||||
* the as well as any extension and uses that as the view name.
|
||||
* @return the default view name to use; if {@code null} is returned
|
||||
* processing will result in an IllegalStateException.
|
||||
* Select a default view name when a controller leaves the view unspecified.
|
||||
* The default implementation strips the leading and trailing slash from the
|
||||
* as well as any extension and uses that as the view name.
|
||||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
protected String getDefaultViewName(HandlerResult result, ServerWebExchange exchange) {
|
||||
String path = this.pathHelper.getLookupPathForRequest(exchange);
|
||||
if (path.startsWith("/")) {
|
||||
|
|
@ -266,79 +263,105 @@ public class ViewResolutionResultHandler extends AbstractHandlerResultHandler
|
|||
return StringUtils.stripFilenameExtension(path);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object updateModel(Object value, HandlerResult result) {
|
||||
if (value instanceof Model) {
|
||||
result.getModel().addAllAttributes(((Model) value).asMap());
|
||||
}
|
||||
else if (value instanceof Map) {
|
||||
result.getModel().addAllAttributes((Map<String, ?>) value);
|
||||
}
|
||||
else {
|
||||
MethodParameter returnType = result.getReturnTypeSource();
|
||||
String name = getNameForReturnValue(value, returnType);
|
||||
result.getModel().addAttribute(name, value);
|
||||
}
|
||||
return value;
|
||||
private Mono<List<View>> resolveViews(String viewName, Locale locale) {
|
||||
return Flux.fromIterable(getViewResolvers())
|
||||
.concatMap(resolver -> resolver.resolveViewName(viewName, locale))
|
||||
.collectList()
|
||||
.map(views -> {
|
||||
if (views.isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"Could not resolve view with name '" + viewName + "'.");
|
||||
}
|
||||
views.addAll(getDefaultViews());
|
||||
return views;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the model attribute name for the given return value using one of:
|
||||
* <ol>
|
||||
* <li>The method {@code ModelAttribute} annotation value
|
||||
* <li>The declared return type if it is more specific than {@code Object}
|
||||
* <li>The actual return value type
|
||||
* </ol>
|
||||
* @param returnValue the value returned from a method invocation
|
||||
* @param returnType the return type of the method
|
||||
* @return the model name, never {@code null} nor empty
|
||||
* Return the name of a model attribute return value based on the method
|
||||
* {@code @ModelAttribute} annotation, if present, or derived from the type
|
||||
* of the return value otherwise.
|
||||
*/
|
||||
private static String getNameForReturnValue(Object returnValue, MethodParameter returnType) {
|
||||
private String getNameForReturnValue(Class<?> returnValueType, MethodParameter returnType) {
|
||||
ModelAttribute annotation = returnType.getMethodAnnotation(ModelAttribute.class);
|
||||
if (annotation != null && StringUtils.hasText(annotation.value())) {
|
||||
return annotation.value();
|
||||
}
|
||||
else {
|
||||
Method method = returnType.getMethod();
|
||||
Class<?> containingClass = returnType.getContainingClass();
|
||||
Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
|
||||
return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue);
|
||||
}
|
||||
// TODO: Conventions does not deal with async wrappers
|
||||
return ClassUtils.getShortNameAsProperty(returnValueType);
|
||||
}
|
||||
|
||||
private Mono<? extends Void> resolveAndRender(String viewName, Locale locale,
|
||||
Map<String, ?> model, ServerWebExchange exchange) {
|
||||
private Mono<Void> resolveAsyncAttributes(Map<String, Object> model) {
|
||||
|
||||
return Flux.fromIterable(getViewResolvers())
|
||||
.concatMap(resolver -> resolver.resolveViewName(viewName, locale))
|
||||
.switchIfEmpty(Mono.error(
|
||||
new IllegalStateException(
|
||||
"Could not resolve view with name '" + viewName + "'.")))
|
||||
.collectList()
|
||||
.then(views -> {
|
||||
views.addAll(getDefaultViews());
|
||||
List<String> names = new ArrayList<>();
|
||||
List<Mono<Object>> valueMonos = new ArrayList<>();
|
||||
|
||||
List<MediaType> producibleTypes = getProducibleMediaTypes(views);
|
||||
MediaType bestMediaType = selectMediaType(exchange, () -> producibleTypes);
|
||||
for (Map.Entry<String, ?> entry : model.entrySet()) {
|
||||
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(null, entry.getValue());
|
||||
if (adapter != null) {
|
||||
names.add(entry.getKey());
|
||||
valueMonos.add(adapter.toMono(entry.getValue()).defaultIfEmpty(NO_VALUE));
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMediaType != null) {
|
||||
for (View view : views) {
|
||||
for (MediaType supported : view.getSupportedMediaTypes()) {
|
||||
if (supported.isCompatibleWith(bestMediaType)) {
|
||||
return view.render(model, bestMediaType, exchange);
|
||||
}
|
||||
}
|
||||
if (names.isEmpty()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
return Mono.when(valueMonos,
|
||||
values -> {
|
||||
for (int i=0; i < values.length; i++) {
|
||||
if (values[i] != NO_VALUE) {
|
||||
model.put(names.get(i), values[i]);
|
||||
}
|
||||
else {
|
||||
model.remove(names.get(i));
|
||||
}
|
||||
}
|
||||
return NO_VALUE;
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
return Mono.error(new NotAcceptableStatusException(producibleTypes));
|
||||
private void addBindingResult(HandlerResult result, ServerWebExchange exchange) {
|
||||
BindingContext context = result.getBindingContext();
|
||||
Map<String, Object> model = context.getModel().asMap();
|
||||
model.keySet().stream()
|
||||
.filter(name -> isBindingCandidate(name, model.get(name)))
|
||||
.filter(name -> !model.containsKey(BindingResult.MODEL_KEY_PREFIX + name))
|
||||
.forEach(name -> {
|
||||
WebExchangeDataBinder binder = context.createDataBinder(exchange, model.get(name), name);
|
||||
model.put(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
|
||||
});
|
||||
}
|
||||
|
||||
private List<MediaType> getProducibleMediaTypes(List<View> views) {
|
||||
List<MediaType> result = new ArrayList<>();
|
||||
views.forEach(view -> result.addAll(view.getSupportedMediaTypes()));
|
||||
return result;
|
||||
private boolean isBindingCandidate(String name, Object value) {
|
||||
return !name.startsWith(BindingResult.MODEL_KEY_PREFIX) && value != null &&
|
||||
!value.getClass().isArray() && !(value instanceof Collection) &&
|
||||
!(value instanceof Map) && !BeanUtils.isSimpleValueType(value.getClass());
|
||||
}
|
||||
|
||||
private Mono<? extends Void> render(List<View> views, Map<String, Object> model,
|
||||
ServerWebExchange exchange) {
|
||||
|
||||
List<MediaType> mediaTypes = getMediaTypes(views);
|
||||
MediaType bestMediaType = selectMediaType(exchange, () -> mediaTypes);
|
||||
if (bestMediaType != null) {
|
||||
for (View view : views) {
|
||||
for (MediaType mediaType : view.getSupportedMediaTypes()) {
|
||||
if (mediaType.isCompatibleWith(bestMediaType)) {
|
||||
return view.render(model, mediaType, exchange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new NotAcceptableStatusException(mediaTypes);
|
||||
}
|
||||
|
||||
private List<MediaType> getMediaTypes(List<View> views) {
|
||||
return views.stream()
|
||||
.flatMap(view -> view.getSupportedMediaTypes().stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,28 +108,13 @@ public class DispatcherHandlerErrorTests {
|
|||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownMethodArgumentType() throws Exception {
|
||||
this.request.setUri("/unknown-argument-type");
|
||||
Mono<Void> publisher = this.dispatcherHandler.handle(this.exchange);
|
||||
|
||||
StepVerifier.create(publisher)
|
||||
.consumeErrorWith(error -> {
|
||||
assertThat(error, instanceOf(IllegalStateException.class));
|
||||
assertThat(error.getMessage(), startsWith("No resolver for argument [0]"));
|
||||
})
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controllerReturnsMonoError() throws Exception {
|
||||
this.request.setUri("/error-signal");
|
||||
Mono<Void> publisher = this.dispatcherHandler.handle(this.exchange);
|
||||
|
||||
StepVerifier.create(publisher)
|
||||
.consumeErrorWith(error -> {
|
||||
assertSame(EXCEPTION, error);
|
||||
})
|
||||
.consumeErrorWith(error -> assertSame(EXCEPTION, error))
|
||||
.verify();
|
||||
}
|
||||
|
||||
|
|
@ -138,10 +123,8 @@ public class DispatcherHandlerErrorTests {
|
|||
this.request.setUri("/raise-exception");
|
||||
Mono<Void> publisher = this.dispatcherHandler.handle(this.exchange);
|
||||
|
||||
StepVerifier.<Void>create(publisher)
|
||||
.consumeErrorWith(error -> {
|
||||
assertSame(EXCEPTION, error);
|
||||
})
|
||||
StepVerifier.create(publisher)
|
||||
.consumeErrorWith(error -> assertSame(EXCEPTION, error))
|
||||
.verify();
|
||||
}
|
||||
|
||||
|
|
@ -164,9 +147,7 @@ public class DispatcherHandlerErrorTests {
|
|||
Mono<Void> publisher = this.dispatcherHandler.handle(this.exchange);
|
||||
|
||||
StepVerifier.create(publisher)
|
||||
.consumeErrorWith(error -> {
|
||||
assertThat(error, instanceOf(NotAcceptableStatusException.class));
|
||||
})
|
||||
.consumeErrorWith(error -> assertThat(error, instanceOf(NotAcceptableStatusException.class)))
|
||||
.verify();
|
||||
}
|
||||
|
||||
|
|
@ -226,10 +207,6 @@ public class DispatcherHandlerErrorTests {
|
|||
@SuppressWarnings("unused")
|
||||
private static class TestController {
|
||||
|
||||
@RequestMapping("/unknown-argument-type")
|
||||
public void unknownArgumentType(Foo arg) {
|
||||
}
|
||||
|
||||
@RequestMapping("/error-signal")
|
||||
@ResponseBody
|
||||
public Publisher<String> errorSignal() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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.reactive.result.method.annotation;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import rx.Single;
|
||||
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.WebExchangeDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.config.WebReactiveConfigurationSupport;
|
||||
import org.springframework.web.reactive.result.ResolvableMethod;
|
||||
import org.springframework.web.reactive.result.method.BindingContext;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link BindingContextFactory}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class BindingContextFactoryTests {
|
||||
|
||||
private BindingContextFactory contextFactory;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
WebReactiveConfigurationSupport configurationSupport = new WebReactiveConfigurationSupport();
|
||||
configurationSupport.setApplicationContext(new StaticApplicationContext());
|
||||
RequestMappingHandlerAdapter adapter = configurationSupport.requestMappingHandlerAdapter();
|
||||
adapter.afterPropertiesSet();
|
||||
this.contextFactory = new BindingContextFactory(adapter);
|
||||
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "/path");
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager manager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(request, response, manager);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void basic() throws Exception {
|
||||
|
||||
Validator validator = mock(Validator.class);
|
||||
TestController controller = new TestController(validator);
|
||||
|
||||
HandlerMethod handlerMethod = ResolvableMethod.on(controller)
|
||||
.annotated(RequestMapping.class)
|
||||
.resolveHandlerMethod();
|
||||
|
||||
BindingContext bindingContext =
|
||||
this.contextFactory.createBindingContext(handlerMethod, this.exchange)
|
||||
.blockMillis(5000);
|
||||
|
||||
WebExchangeDataBinder binder = bindingContext.createDataBinder(this.exchange, "name");
|
||||
assertEquals(Collections.singletonList(validator), binder.getValidators());
|
||||
|
||||
Map<String, Object> model = bindingContext.getModel().asMap();
|
||||
assertEquals(5, model.size());
|
||||
|
||||
Object value = model.get("bean");
|
||||
assertEquals("Bean", ((TestBean) value).getName());
|
||||
|
||||
value = model.get("monoBean");
|
||||
assertEquals("Mono Bean", ((Mono<TestBean>) value).blockMillis(5000).getName());
|
||||
|
||||
value = model.get("singleBean");
|
||||
assertEquals("Single Bean", ((Single<TestBean>) value).toBlocking().value().getName());
|
||||
|
||||
value = model.get("voidMethodBean");
|
||||
assertEquals("Void Method Bean", ((TestBean) value).getName());
|
||||
|
||||
value = model.get("voidMonoMethodBean");
|
||||
assertEquals("Void Mono Method Bean", ((TestBean) value).getName());
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class TestController {
|
||||
|
||||
private Validator[] validators;
|
||||
|
||||
|
||||
public TestController(Validator... validators) {
|
||||
this.validators = validators;
|
||||
}
|
||||
|
||||
|
||||
@InitBinder
|
||||
public void initDataBinder(WebDataBinder dataBinder) {
|
||||
if (!ObjectUtils.isEmpty(this.validators)) {
|
||||
dataBinder.addValidators(this.validators);
|
||||
}
|
||||
}
|
||||
|
||||
@ModelAttribute("bean")
|
||||
public TestBean returnValue() {
|
||||
return new TestBean("Bean");
|
||||
}
|
||||
|
||||
@ModelAttribute("monoBean")
|
||||
public Mono<TestBean> returnValueMono() {
|
||||
return Mono.just(new TestBean("Mono Bean"));
|
||||
}
|
||||
|
||||
@ModelAttribute("singleBean")
|
||||
public Single<TestBean> returnValueSingle() {
|
||||
return Single.just(new TestBean("Single Bean"));
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public void voidMethodBean(Model model) {
|
||||
model.addAttribute("voidMethodBean", new TestBean("Void Method Bean"));
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public Mono<Void> voidMonoMethodBean(Model model) {
|
||||
return Mono.just("Void Mono Method Bean")
|
||||
.doOnNext(name -> model.addAttribute("voidMonoMethodBean", new TestBean(name)))
|
||||
.then();
|
||||
}
|
||||
|
||||
@RequestMapping
|
||||
public void handle() {}
|
||||
}
|
||||
|
||||
private static class TestBean {
|
||||
|
||||
private final String name;
|
||||
|
||||
TestBean(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TestBean[name=" + this.name + "]";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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.reactive.result.method.annotation;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoProcessor;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.web.bind.WebExchangeDataBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.reactive.result.ResolvableMethod;
|
||||
import org.springframework.web.reactive.result.method.BindingContext;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.MockWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static junit.framework.TestCase.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.springframework.core.ResolvableType.forClass;
|
||||
import static org.springframework.core.ResolvableType.forClassWithGenerics;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ErrorsMethodArgumentResolver}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ErrorsArgumentResolverTests {
|
||||
|
||||
private ErrorsMethodArgumentResolver resolver ;
|
||||
|
||||
private final BindingContext bindingContext = new BindingContext();
|
||||
|
||||
private BindingResult bindingResult;
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private final ResolvableMethod testMethod = ResolvableMethod.onClass(this.getClass()).name("handle");
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
this.resolver = new ErrorsMethodArgumentResolver(new ReactiveAdapterRegistry());
|
||||
|
||||
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "/path");
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
WebSessionManager manager = new MockWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(request, response, manager);
|
||||
|
||||
Foo foo = new Foo();
|
||||
WebExchangeDataBinder binder = this.bindingContext.createDataBinder(this.exchange, foo, "foo");
|
||||
this.bindingResult = binder.getBindingResult();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void supports() throws Exception {
|
||||
|
||||
MethodParameter parameter = parameter(forClass(Errors.class));
|
||||
assertTrue(this.resolver.supportsParameter(parameter));
|
||||
|
||||
parameter = parameter(forClass(BindingResult.class));
|
||||
assertTrue(this.resolver.supportsParameter(parameter));
|
||||
|
||||
parameter = parameter(forClassWithGenerics(Mono.class, Errors.class));
|
||||
assertFalse(this.resolver.supportsParameter(parameter));
|
||||
|
||||
parameter = parameter(forClass(String.class));
|
||||
assertFalse(this.resolver.supportsParameter(parameter));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveErrors() throws Exception {
|
||||
testResolve(this.bindingResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveErrorsMono() throws Exception {
|
||||
MonoProcessor<BindingResult> monoProcessor = MonoProcessor.create();
|
||||
monoProcessor.onNext(this.bindingResult);
|
||||
testResolve(monoProcessor);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void resolveErrorsAfterMonoModelAttribute() throws Exception {
|
||||
MethodParameter parameter = parameter(forClass(BindingResult.class));
|
||||
this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange).blockMillis(5000);
|
||||
}
|
||||
|
||||
|
||||
private void testResolve(Object bindingResult) {
|
||||
|
||||
String key = BindingResult.MODEL_KEY_PREFIX + "foo";
|
||||
this.bindingContext.getModel().asMap().put(key, bindingResult);
|
||||
|
||||
MethodParameter parameter = parameter(forClass(Errors.class));
|
||||
|
||||
Object actual = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange)
|
||||
.blockMillis(5000);
|
||||
|
||||
assertSame(this.bindingResult, actual);
|
||||
}
|
||||
|
||||
|
||||
private MethodParameter parameter(ResolvableType type) {
|
||||
return this.testMethod.resolveParam(type);
|
||||
}
|
||||
|
||||
|
||||
private static class Foo {
|
||||
|
||||
private String name;
|
||||
|
||||
public Foo() {
|
||||
}
|
||||
|
||||
public Foo(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void handle(
|
||||
@ModelAttribute Foo foo,
|
||||
Errors errors,
|
||||
@ModelAttribute Mono<Foo> fooMono,
|
||||
BindingResult bindingResult,
|
||||
Mono<Errors> errorsMono,
|
||||
String string) {}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
/*
|
||||
* 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.reactive.result.method.annotation;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import rx.RxReactiveStreams;
|
||||
import rx.Single;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import org.springframework.web.bind.WebExchangeBindException;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||
import org.springframework.web.reactive.result.ResolvableMethod;
|
||||
import org.springframework.web.reactive.result.method.BindingContext;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.MockWebSessionManager;
|
||||
|
||||
import static junit.framework.TestCase.assertNotNull;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.springframework.core.ResolvableType.forClass;
|
||||
import static org.springframework.core.ResolvableType.forClassWithGenerics;
|
||||
import static org.springframework.util.Assert.isTrue;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ModelAttributeMethodArgumentResolver}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ModelAttributeMethodArgumentResolverTests {
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private final MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, "/path");
|
||||
|
||||
private final MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
|
||||
|
||||
private BindingContext bindingContext;
|
||||
|
||||
private ResolvableMethod testMethod = ResolvableMethod.onClass(this.getClass()).name("handle");
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
this.exchange = new DefaultServerWebExchange(this.request, response, new MockWebSessionManager());
|
||||
this.exchange = this.exchange.mutate().setFormData(Mono.just(this.formData)).build();
|
||||
|
||||
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
|
||||
validator.afterPropertiesSet();
|
||||
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
|
||||
initializer.setValidator(validator);
|
||||
this.bindingContext = new BindingContext(initializer);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void supports() throws Exception {
|
||||
|
||||
ModelAttributeMethodArgumentResolver resolver =
|
||||
new ModelAttributeMethodArgumentResolver(new ReactiveAdapterRegistry(), false);
|
||||
|
||||
ResolvableType type = forClass(Foo.class);
|
||||
assertTrue(resolver.supportsParameter(parameter(type)));
|
||||
|
||||
type = forClassWithGenerics(Mono.class, Foo.class);
|
||||
assertTrue(resolver.supportsParameter(parameter(type)));
|
||||
|
||||
type = forClass(Foo.class);
|
||||
assertFalse(resolver.supportsParameter(parameterNotAnnotated(type)));
|
||||
|
||||
type = forClassWithGenerics(Mono.class, Foo.class);
|
||||
assertFalse(resolver.supportsParameter(parameterNotAnnotated(type)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsWithDefaultResolution() throws Exception {
|
||||
|
||||
ModelAttributeMethodArgumentResolver resolver =
|
||||
new ModelAttributeMethodArgumentResolver(new ReactiveAdapterRegistry(), true);
|
||||
|
||||
ResolvableType type = forClass(Foo.class);
|
||||
assertTrue(resolver.supportsParameter(parameterNotAnnotated(type)));
|
||||
|
||||
type = forClassWithGenerics(Mono.class, Foo.class);
|
||||
assertTrue(resolver.supportsParameter(parameterNotAnnotated(type)));
|
||||
|
||||
type = forClass(String.class);
|
||||
assertFalse(resolver.supportsParameter(parameterNotAnnotated(type)));
|
||||
|
||||
type = forClassWithGenerics(Mono.class, String.class);
|
||||
assertFalse(resolver.supportsParameter(parameterNotAnnotated(type)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createAndBind() throws Exception {
|
||||
testBindFoo(forClass(Foo.class), value -> {
|
||||
assertEquals(Foo.class, value.getClass());
|
||||
return (Foo) value;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createAndBindToMono() throws Exception {
|
||||
testBindFoo(forClassWithGenerics(Mono.class, Foo.class), mono -> {
|
||||
assertTrue(mono.getClass().getName(), mono instanceof Mono);
|
||||
Object value = ((Mono<?>) mono).blockMillis(5000);
|
||||
assertEquals(Foo.class, value.getClass());
|
||||
return (Foo) value;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createAndBindToSingle() throws Exception {
|
||||
testBindFoo(forClassWithGenerics(Single.class, Foo.class), single -> {
|
||||
assertTrue(single.getClass().getName(), single instanceof Single);
|
||||
Object value = ((Single<?>) single).toBlocking().value();
|
||||
assertEquals(Foo.class, value.getClass());
|
||||
return (Foo) value;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindExisting() throws Exception {
|
||||
Foo foo = new Foo();
|
||||
foo.setName("Jim");
|
||||
this.bindingContext.getModel().addAttribute(foo);
|
||||
|
||||
testBindFoo(forClass(Foo.class), value -> {
|
||||
assertEquals(Foo.class, value.getClass());
|
||||
return (Foo) value;
|
||||
});
|
||||
|
||||
assertSame(foo, this.bindingContext.getModel().asMap().get("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindExistingMono() throws Exception {
|
||||
Foo foo = new Foo();
|
||||
foo.setName("Jim");
|
||||
this.bindingContext.getModel().addAttribute("foo", Mono.just(foo));
|
||||
|
||||
testBindFoo(forClass(Foo.class), value -> {
|
||||
assertEquals(Foo.class, value.getClass());
|
||||
return (Foo) value;
|
||||
});
|
||||
|
||||
assertSame(foo, this.bindingContext.getModel().asMap().get("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindExistingSingle() throws Exception {
|
||||
Foo foo = new Foo();
|
||||
foo.setName("Jim");
|
||||
this.bindingContext.getModel().addAttribute("foo", Single.just(foo));
|
||||
|
||||
testBindFoo(forClass(Foo.class), value -> {
|
||||
assertEquals(Foo.class, value.getClass());
|
||||
return (Foo) value;
|
||||
});
|
||||
|
||||
assertSame(foo, this.bindingContext.getModel().asMap().get("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindExistingMonoToMono() throws Exception {
|
||||
Foo foo = new Foo();
|
||||
foo.setName("Jim");
|
||||
this.bindingContext.getModel().addAttribute("foo", Mono.just(foo));
|
||||
|
||||
testBindFoo(forClassWithGenerics(Mono.class, Foo.class), mono -> {
|
||||
assertTrue(mono.getClass().getName(), mono instanceof Mono);
|
||||
Object value = ((Mono<?>) mono).blockMillis(5000);
|
||||
assertEquals(Foo.class, value.getClass());
|
||||
return (Foo) value;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validationError() throws Exception {
|
||||
testValidationError(forClass(Foo.class), resolvedArgumentMono -> resolvedArgumentMono);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void validationErrorToMono() throws Exception {
|
||||
testValidationError(forClassWithGenerics(Mono.class, Foo.class),
|
||||
resolvedArgumentMono -> {
|
||||
Object value = resolvedArgumentMono.blockMillis(5000);
|
||||
assertNotNull(value);
|
||||
isTrue(value instanceof Mono);
|
||||
return (Mono<?>) value;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void validationErrorToSingle() throws Exception {
|
||||
testValidationError(forClassWithGenerics(Single.class, Foo.class),
|
||||
resolvedArgumentMono -> {
|
||||
Object value = resolvedArgumentMono.blockMillis(5000);
|
||||
assertNotNull(value);
|
||||
isTrue(value instanceof Single);
|
||||
return Mono.from(RxReactiveStreams.toPublisher((Single) value));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void testBindFoo(ResolvableType type, Function<Object, Foo> valueExtractor) {
|
||||
|
||||
this.formData.add("name", "Robert");
|
||||
this.formData.add("age", "25");
|
||||
|
||||
Object value = createResolver()
|
||||
.resolveArgument(parameter(type), this.bindingContext, this.exchange)
|
||||
.blockMillis(5000);
|
||||
|
||||
Foo foo = valueExtractor.apply(value);
|
||||
assertEquals("Robert", foo.getName());
|
||||
|
||||
String key = "foo";
|
||||
String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key;
|
||||
|
||||
Map<String, Object> map = bindingContext.getModel().asMap();
|
||||
assertEquals(map.toString(), 2, map.size());
|
||||
assertSame(foo, map.get(key));
|
||||
assertNotNull(map.get(bindingResultKey));
|
||||
assertTrue(map.get(bindingResultKey) instanceof BindingResult);
|
||||
}
|
||||
|
||||
private void testValidationError(ResolvableType type, Function<Mono<?>, Mono<?>> valueMonoExtractor) {
|
||||
|
||||
this.formData.add("age", "invalid");
|
||||
|
||||
HandlerMethodArgumentResolver resolver = createResolver();
|
||||
Mono<?> mono = resolver.resolveArgument(parameter(type), this.bindingContext, this.exchange);
|
||||
|
||||
mono = valueMonoExtractor.apply(mono);
|
||||
|
||||
StepVerifier.create(mono)
|
||||
.consumeErrorWith(ex -> {
|
||||
assertTrue(ex instanceof WebExchangeBindException);
|
||||
WebExchangeBindException bindException = (WebExchangeBindException) ex;
|
||||
assertEquals(1, bindException.getErrorCount());
|
||||
assertTrue(bindException.hasFieldErrors("age"));
|
||||
})
|
||||
.verify();
|
||||
}
|
||||
|
||||
|
||||
private ModelAttributeMethodArgumentResolver createResolver() {
|
||||
return new ModelAttributeMethodArgumentResolver(new ReactiveAdapterRegistry());
|
||||
}
|
||||
|
||||
private MethodParameter parameter(ResolvableType type) {
|
||||
return this.testMethod.resolveParam(type,
|
||||
parameter -> parameter.hasParameterAnnotation(ModelAttribute.class));
|
||||
}
|
||||
|
||||
private MethodParameter parameterNotAnnotated(ResolvableType type) {
|
||||
return this.testMethod.resolveParam(type,
|
||||
parameter -> !parameter.hasParameterAnnotations());
|
||||
}
|
||||
|
||||
|
||||
private static class Foo {
|
||||
|
||||
private String name;
|
||||
|
||||
private int age;
|
||||
|
||||
public Foo() {
|
||||
}
|
||||
|
||||
public Foo(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return this.age;
|
||||
}
|
||||
|
||||
public void setAge(int age) {
|
||||
this.age = age;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void handle(
|
||||
@ModelAttribute @Validated Foo foo,
|
||||
@ModelAttribute @Validated Mono<Foo> mono,
|
||||
@ModelAttribute @Validated Single<Foo> single,
|
||||
Foo fooNotAnnotated,
|
||||
String stringNotAnnotated,
|
||||
Mono<Foo> monoNotAnnotated,
|
||||
Mono<String> monoStringNotAnnotated) {}
|
||||
|
||||
}
|
||||
|
|
@ -18,8 +18,10 @@ package org.springframework.web.reactive.result.method.annotation;
|
|||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.propertyeditors.CustomDateEditor;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
|
@ -27,12 +29,17 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
|
|||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.reactive.config.EnableWebReactive;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
|
@ -62,6 +69,18 @@ public class RequestMappingDataBindingIntegrationTests extends AbstractRequestMa
|
|||
new HttpHeaders(), null, String.class).getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleForm() throws Exception {
|
||||
|
||||
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
|
||||
formData.add("name", "George");
|
||||
formData.add("age", "5");
|
||||
|
||||
assertEquals("Processed form: Foo[id=1, name='George', age=5]",
|
||||
performPost("/foos/1", MediaType.APPLICATION_FORM_URLENCODED, formData,
|
||||
MediaType.TEXT_PLAIN, String.class).getBody());
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableWebReactive
|
||||
|
|
@ -70,21 +89,73 @@ public class RequestMappingDataBindingIntegrationTests extends AbstractRequestMa
|
|||
static class WebConfig {
|
||||
}
|
||||
|
||||
@Controller
|
||||
@SuppressWarnings("unused")
|
||||
@RestController
|
||||
@SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"})
|
||||
private static class TestController {
|
||||
|
||||
@InitBinder
|
||||
public void initBinder(WebDataBinder dataBinder, @RequestParam("date-pattern") String pattern) {
|
||||
CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(pattern), false);
|
||||
dataBinder.registerCustomEditor(Date.class, dateEditor);
|
||||
public void initBinder(WebDataBinder binder,
|
||||
@RequestParam("date-pattern") Optional<String> optionalPattern) {
|
||||
|
||||
optionalPattern.ifPresent(pattern -> {
|
||||
CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(pattern), false);
|
||||
binder.registerCustomEditor(Date.class, dateEditor);
|
||||
});
|
||||
}
|
||||
|
||||
@PostMapping("/date-param")
|
||||
@ResponseBody
|
||||
public String handleDateParam(@RequestParam Date date) {
|
||||
return "Processed date!";
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public Mono<Foo> addFooAttribute(@PathVariable("id") Optional<Long> optiponalId) {
|
||||
return optiponalId.map(id -> Mono.just(new Foo(id))).orElse(Mono.empty());
|
||||
}
|
||||
|
||||
@PostMapping("/foos/{id}")
|
||||
public String handleForm(@ModelAttribute Foo foo, Errors errors) {
|
||||
return (errors.hasErrors() ?
|
||||
"Form not processed" : "Processed form: " + foo);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Foo {
|
||||
|
||||
private final Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private int age;
|
||||
|
||||
public Foo(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return this.age;
|
||||
}
|
||||
|
||||
public void setAge(int age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Foo[id=" + this.id + ", name='" + this.name + "', age=" + this.age + "]";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,11 +37,10 @@ import org.springframework.http.codec.HttpMessageWriter;
|
|||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
|
@ -121,13 +120,13 @@ public class ResponseBodyResultHandlerTests {
|
|||
public void writeResponseStatus() throws NoSuchMethodException {
|
||||
Object controller = new TestRestController();
|
||||
HandlerMethod hm = handlerMethod(controller, "handleToString");
|
||||
HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap());
|
||||
HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType());
|
||||
|
||||
StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify();
|
||||
assertEquals(HttpStatus.NO_CONTENT, this.response.getStatusCode());
|
||||
|
||||
hm = handlerMethod(controller, "handleToMonoVoid");
|
||||
handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap());
|
||||
handlerResult = new HandlerResult(hm, null, hm.getReturnType());
|
||||
|
||||
StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify();
|
||||
assertEquals(HttpStatus.CREATED, this.response.getStatusCode());
|
||||
|
|
@ -135,7 +134,7 @@ public class ResponseBodyResultHandlerTests {
|
|||
|
||||
private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException {
|
||||
HandlerMethod hm = handlerMethod(controller, method);
|
||||
HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap());
|
||||
HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType());
|
||||
assertEquals(result, this.resultHandler.supports(handlerResult));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package org.springframework.web.reactive.result.view;
|
|||
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
|
@ -26,6 +25,7 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
|
@ -40,52 +40,52 @@ import org.springframework.core.Ordered;
|
|||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ConcurrentModel;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
import org.springframework.web.reactive.result.ResolvableMethod;
|
||||
import org.springframework.web.reactive.result.method.BindingContext;
|
||||
import org.springframework.web.server.NotAcceptableStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.adapter.DefaultServerWebExchange;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.core.ResolvableType.forClass;
|
||||
import static org.springframework.core.ResolvableType.forClassWithGenerics;
|
||||
import static org.springframework.core.io.buffer.support.DataBufferTestUtils.dumpString;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ViewResolutionResultHandler}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ViewResolutionResultHandlerTests {
|
||||
|
||||
private MockServerHttpRequest request;
|
||||
private final MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "/path");
|
||||
|
||||
private MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
private final MockServerHttpResponse response = new MockServerHttpResponse();
|
||||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private ModelMap model = new ExtendedModelMap();
|
||||
private final BindingContext bindingContext = new BindingContext();
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
this.request = new MockServerHttpRequest(HttpMethod.GET, "/path");
|
||||
WebSessionManager manager = new DefaultWebSessionManager();
|
||||
this.exchange = new DefaultServerWebExchange(this.request, this.response, manager);
|
||||
}
|
||||
|
|
@ -93,21 +93,30 @@ public class ViewResolutionResultHandlerTests {
|
|||
|
||||
@Test
|
||||
public void supports() throws Exception {
|
||||
testSupports(forClass(String.class), true);
|
||||
testSupports(forClass(View.class), true);
|
||||
testSupports(forClassWithGenerics(Mono.class, String.class), true);
|
||||
testSupports(forClassWithGenerics(Mono.class, View.class), true);
|
||||
testSupports(forClassWithGenerics(Single.class, String.class), true);
|
||||
testSupports(forClassWithGenerics(Single.class, View.class), true);
|
||||
testSupports(forClassWithGenerics(Mono.class, Void.class), true);
|
||||
testSupports(forClass(Completable.class), true);
|
||||
testSupports(forClass(Model.class), true);
|
||||
testSupports(forClass(Map.class), true);
|
||||
testSupports(forClass(TestBean.class), true);
|
||||
testSupports(forClass(Integer.class), false);
|
||||
testSupports(resolvableMethod().annotated(ModelAttribute.class), true);
|
||||
}
|
||||
|
||||
testSupports(ResolvableType.forClass(String.class), true);
|
||||
testSupports(ResolvableType.forClass(View.class), true);
|
||||
testSupports(ResolvableType.forClassWithGenerics(Mono.class, String.class), true);
|
||||
testSupports(ResolvableType.forClassWithGenerics(Mono.class, View.class), true);
|
||||
testSupports(ResolvableType.forClassWithGenerics(Single.class, String.class), true);
|
||||
testSupports(ResolvableType.forClassWithGenerics(Single.class, View.class), true);
|
||||
testSupports(ResolvableType.forClassWithGenerics(Mono.class, Void.class), true);
|
||||
testSupports(ResolvableType.forClass(Completable.class), true);
|
||||
testSupports(ResolvableType.forClass(Model.class), true);
|
||||
testSupports(ResolvableType.forClass(Map.class), true);
|
||||
testSupports(ResolvableType.forClass(TestBean.class), true);
|
||||
testSupports(ResolvableType.forClass(Integer.class), false);
|
||||
private void testSupports(ResolvableType type, boolean result) {
|
||||
testSupports(resolvableMethod().returning(type), result);
|
||||
}
|
||||
|
||||
testSupports(ResolvableMethod.onClass(TestController.class).annotated(ModelAttribute.class), true);
|
||||
private void testSupports(ResolvableMethod resolvableMethod, boolean result) {
|
||||
ViewResolutionResultHandler resultHandler = resultHandler(mock(ViewResolver.class));
|
||||
MethodParameter returnType = resolvableMethod.resolveReturnType();
|
||||
HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, this.bindingContext);
|
||||
assertEquals(result, resultHandler.supports(handlerResult));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -116,7 +125,7 @@ public class ViewResolutionResultHandlerTests {
|
|||
TestViewResolver resolver2 = new TestViewResolver("profile");
|
||||
resolver1.setOrder(2);
|
||||
resolver2.setOrder(1);
|
||||
List<ViewResolver> resolvers = createResultHandler(resolver1, resolver2).getViewResolvers();
|
||||
List<ViewResolver> resolvers = resultHandler(resolver1, resolver2).getViewResolvers();
|
||||
|
||||
assertEquals(Arrays.asList(resolver2, resolver1), resolvers);
|
||||
}
|
||||
|
|
@ -127,47 +136,52 @@ public class ViewResolutionResultHandlerTests {
|
|||
ResolvableType returnType;
|
||||
ViewResolver resolver = new TestViewResolver("account");
|
||||
|
||||
returnType = ResolvableType.forClass(View.class);
|
||||
returnType = forClass(View.class);
|
||||
returnValue = new TestView("account");
|
||||
testHandle("/path", returnType, returnValue, "account: {id=123}");
|
||||
assertEquals(HttpStatus.NO_CONTENT, this.exchange.getResponse().getStatusCode());
|
||||
|
||||
returnType = ResolvableType.forClassWithGenerics(Mono.class, View.class);
|
||||
returnType = forClassWithGenerics(Mono.class, View.class);
|
||||
returnValue = Mono.just(new TestView("account"));
|
||||
testHandle("/path", returnType, returnValue, "account: {id=123}");
|
||||
assertEquals(HttpStatus.SEE_OTHER, this.exchange.getResponse().getStatusCode());
|
||||
|
||||
returnType = ResolvableType.forClass(String.class);
|
||||
returnType = forClass(String.class);
|
||||
returnValue = "account";
|
||||
testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
|
||||
assertEquals(HttpStatus.CREATED, this.exchange.getResponse().getStatusCode());
|
||||
|
||||
returnType = ResolvableType.forClassWithGenerics(Mono.class, String.class);
|
||||
returnType = forClassWithGenerics(Mono.class, String.class);
|
||||
returnValue = Mono.just("account");
|
||||
testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
|
||||
assertEquals(HttpStatus.PARTIAL_CONTENT, this.exchange.getResponse().getStatusCode());
|
||||
|
||||
returnType = ResolvableType.forClass(Model.class);
|
||||
returnValue = new ExtendedModelMap().addAttribute("name", "Joe");
|
||||
returnType = forClass(Model.class);
|
||||
returnValue = new ConcurrentModel().addAttribute("name", "Joe");
|
||||
testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver);
|
||||
|
||||
returnType = ResolvableType.forClass(Map.class);
|
||||
returnType = forClass(Map.class);
|
||||
returnValue = Collections.singletonMap("name", "Joe");
|
||||
testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver);
|
||||
|
||||
returnType = ResolvableType.forClass(TestBean.class);
|
||||
returnType = forClass(TestBean.class);
|
||||
returnValue = new TestBean("Joe");
|
||||
String responseBody = "account: {id=123, testBean=TestBean[name=Joe]}";
|
||||
String responseBody = "account: {" +
|
||||
"id=123, " +
|
||||
"org.springframework.validation.BindingResult.testBean=" +
|
||||
"org.springframework.validation.BeanPropertyBindingResult: 0 errors, " +
|
||||
"testBean=TestBean[name=Joe]" +
|
||||
"}";
|
||||
testHandle("/account", returnType, returnValue, responseBody, resolver);
|
||||
|
||||
testHandle("/account", ResolvableMethod.onClass(TestController.class).annotated(ModelAttribute.class),
|
||||
testHandle("/account", resolvableMethod().annotated(ModelAttribute.class),
|
||||
99L, "account: {id=123, num=99}", resolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleWithMultipleResolvers() throws Exception {
|
||||
Object returnValue = "profile";
|
||||
ResolvableType returnType = ResolvableType.forClass(String.class);
|
||||
ResolvableType returnType = forClass(String.class);
|
||||
ViewResolver[] resolvers = {new TestViewResolver("account"), new TestViewResolver("profile")};
|
||||
|
||||
testHandle("/account", returnType, returnValue, "profile: {id=123}", resolvers);
|
||||
|
|
@ -175,76 +189,78 @@ public class ViewResolutionResultHandlerTests {
|
|||
|
||||
@Test
|
||||
public void defaultViewName() throws Exception {
|
||||
testDefaultViewName(null, ResolvableType.forClass(String.class));
|
||||
testDefaultViewName(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, String.class));
|
||||
testDefaultViewName(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, Void.class));
|
||||
testDefaultViewName(Completable.complete(), ResolvableType.forClass(Completable.class));
|
||||
testDefaultViewName(null, forClass(String.class));
|
||||
testDefaultViewName(Mono.empty(), forClassWithGenerics(Mono.class, String.class));
|
||||
testDefaultViewName(Mono.empty(), forClassWithGenerics(Mono.class, Void.class));
|
||||
testDefaultViewName(Completable.complete(), forClass(Completable.class));
|
||||
}
|
||||
|
||||
private void testDefaultViewName(Object returnValue, ResolvableType type)
|
||||
throws URISyntaxException {
|
||||
|
||||
ModelMap model = new ExtendedModelMap().addAttribute("id", "123");
|
||||
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), model);
|
||||
ViewResolutionResultHandler handler = createResultHandler(new TestViewResolver("account"));
|
||||
private void testDefaultViewName(Object returnValue, ResolvableType type) throws URISyntaxException {
|
||||
this.bindingContext.getModel().addAttribute("id", "123");
|
||||
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), this.bindingContext);
|
||||
ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account"));
|
||||
|
||||
this.request.setUri("/account");
|
||||
handler.handleResult(this.exchange, result).block(Duration.ofSeconds(5));
|
||||
handler.handleResult(this.exchange, result).blockMillis(5000);
|
||||
assertResponseBody("account: {id=123}");
|
||||
|
||||
this.request.setUri("/account/");
|
||||
handler.handleResult(this.exchange, result).block(Duration.ofSeconds(5));
|
||||
handler.handleResult(this.exchange, result).blockMillis(5000);
|
||||
assertResponseBody("account: {id=123}");
|
||||
|
||||
this.request.setUri("/account.123");
|
||||
handler.handleResult(this.exchange, result).block(Duration.ofSeconds(5));
|
||||
handler.handleResult(this.exchange, result).blockMillis(5000);
|
||||
assertResponseBody("account: {id=123}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unresolvedViewName() throws Exception {
|
||||
String returnValue = "account";
|
||||
ResolvableType type = ResolvableType.forClass(String.class);
|
||||
HandlerResult handlerResult = new HandlerResult(new Object(), returnValue, returnType(type), this.model);
|
||||
MethodParameter returnType = returnType(forClass(String.class));
|
||||
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.bindingContext);
|
||||
|
||||
this.request.setUri("/path");
|
||||
Mono<Void> mono = createResultHandler().handleResult(this.exchange, handlerResult);
|
||||
Mono<Void> mono = resultHandler().handleResult(this.exchange, result);
|
||||
|
||||
StepVerifier.create(mono)
|
||||
.expectNextCount(0)
|
||||
.expectErrorMatches(err -> err.getMessage().equals("Could not resolve view with name 'account'."))
|
||||
.expectErrorMessage("Could not resolve view with name 'account'.")
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contentNegotiation() throws Exception {
|
||||
TestBean value = new TestBean("Joe");
|
||||
ResolvableType type = ResolvableType.forClass(TestBean.class);
|
||||
HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), this.model);
|
||||
MethodParameter returnType = returnType(forClass(TestBean.class));
|
||||
HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType, this.bindingContext);
|
||||
|
||||
this.request.setHeader("Accept", "application/json");
|
||||
this.request.setUri("/account");
|
||||
|
||||
TestView defaultView = new TestView("jsonView", APPLICATION_JSON);
|
||||
|
||||
createResultHandler(Collections.singletonList(defaultView), new TestViewResolver("account"))
|
||||
resultHandler(Collections.singletonList(defaultView), new TestViewResolver("account"))
|
||||
.handleResult(this.exchange, handlerResult)
|
||||
.block(Duration.ofSeconds(5));
|
||||
|
||||
assertEquals(APPLICATION_JSON, this.response.getHeaders().getContentType());
|
||||
assertResponseBody("jsonView: {testBean=TestBean[name=Joe]}");
|
||||
assertResponseBody("jsonView: {" +
|
||||
"org.springframework.validation.BindingResult.testBean=" +
|
||||
"org.springframework.validation.BeanPropertyBindingResult: 0 errors, " +
|
||||
"testBean=TestBean[name=Joe]" +
|
||||
"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contentNegotiationWith406() throws Exception {
|
||||
TestBean value = new TestBean("Joe");
|
||||
ResolvableType type = ResolvableType.forClass(TestBean.class);
|
||||
HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), this.model);
|
||||
MethodParameter returnType = returnType(forClass(TestBean.class));
|
||||
HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType, this.bindingContext);
|
||||
|
||||
this.request.setHeader("Accept", "application/json");
|
||||
this.request.setUri("/account");
|
||||
|
||||
ViewResolutionResultHandler resultHandler = createResultHandler(new TestViewResolver("account"));
|
||||
ViewResolutionResultHandler resultHandler = resultHandler(new TestViewResolver("account"));
|
||||
Mono<Void> mono = resultHandler.handleResult(this.exchange, handlerResult);
|
||||
StepVerifier.create(mono)
|
||||
.expectNextCount(0)
|
||||
|
|
@ -252,16 +268,39 @@ public class ViewResolutionResultHandlerTests {
|
|||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void modelWithAsyncAttributes() throws Exception {
|
||||
this.bindingContext.getModel()
|
||||
.addAttribute("bean1", Mono.just(new TestBean("Bean1")))
|
||||
.addAttribute("bean2", Single.just(new TestBean("Bean2")))
|
||||
.addAttribute("empty", Mono.empty());
|
||||
|
||||
ResolvableType type = forClass(void.class);
|
||||
HandlerResult result = new HandlerResult(new Object(), null, returnType(type), this.bindingContext);
|
||||
ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account"));
|
||||
|
||||
this.request.setUri("/account");
|
||||
handler.handleResult(this.exchange, result).blockMillis(5000);
|
||||
assertResponseBody("account: {" +
|
||||
"bean1=TestBean[name=Bean1], " +
|
||||
"bean2=TestBean[name=Bean2], " +
|
||||
"org.springframework.validation.BindingResult.bean1=" +
|
||||
"org.springframework.validation.BeanPropertyBindingResult: 0 errors, " +
|
||||
"org.springframework.validation.BindingResult.bean2=" +
|
||||
"org.springframework.validation.BeanPropertyBindingResult: 0 errors" +
|
||||
"}");
|
||||
}
|
||||
|
||||
|
||||
private MethodParameter returnType(ResolvableType type) {
|
||||
return ResolvableMethod.onClass(TestController.class).returning(type).resolveReturnType();
|
||||
return resolvableMethod().returning(type).resolveReturnType();
|
||||
}
|
||||
|
||||
private ViewResolutionResultHandler createResultHandler(ViewResolver... resolvers) {
|
||||
return createResultHandler(Collections.emptyList(), resolvers);
|
||||
private ViewResolutionResultHandler resultHandler(ViewResolver... resolvers) {
|
||||
return resultHandler(Collections.emptyList(), resolvers);
|
||||
}
|
||||
|
||||
private ViewResolutionResultHandler createResultHandler(List<View> defaultViews, ViewResolver... resolvers) {
|
||||
private ViewResolutionResultHandler resultHandler(List<View> defaultViews, ViewResolver... resolvers) {
|
||||
List<ViewResolver> resolverList = Arrays.asList(resolvers);
|
||||
RequestedContentTypeResolver contentTypeResolver = new HeaderContentTypeResolver();
|
||||
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolverList, contentTypeResolver);
|
||||
|
|
@ -269,39 +308,32 @@ public class ViewResolutionResultHandlerTests {
|
|||
return handler;
|
||||
}
|
||||
|
||||
private void testSupports(ResolvableType type, boolean result) {
|
||||
testSupports(ResolvableMethod.onClass(TestController.class).returning(type), result);
|
||||
}
|
||||
|
||||
private void testSupports(ResolvableMethod resolvableMethod, boolean result) {
|
||||
ViewResolutionResultHandler resultHandler = createResultHandler(mock(ViewResolver.class));
|
||||
MethodParameter returnType = resolvableMethod.resolveReturnType();
|
||||
HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, this.model);
|
||||
assertEquals(result, resultHandler.supports(handlerResult));
|
||||
private ResolvableMethod resolvableMethod() {
|
||||
return ResolvableMethod.onClass(TestController.class);
|
||||
}
|
||||
|
||||
private void testHandle(String path, ResolvableType returnType, Object returnValue,
|
||||
String responseBody, ViewResolver... resolvers) throws URISyntaxException {
|
||||
|
||||
testHandle(path, ResolvableMethod.onClass(TestController.class).returning(returnType),
|
||||
returnValue, responseBody, resolvers);
|
||||
testHandle(path, resolvableMethod().returning(returnType), returnValue, responseBody, resolvers);
|
||||
}
|
||||
|
||||
private void testHandle(String path, ResolvableMethod resolvableMethod, Object returnValue,
|
||||
String responseBody, ViewResolver... resolvers) throws URISyntaxException {
|
||||
|
||||
ModelMap model = new ExtendedModelMap().addAttribute("id", "123");
|
||||
Model model = this.bindingContext.getModel();
|
||||
model.asMap().clear();
|
||||
model.addAttribute("id", "123");
|
||||
MethodParameter returnType = resolvableMethod.resolveReturnType();
|
||||
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, model);
|
||||
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.bindingContext);
|
||||
this.request.setUri(path);
|
||||
createResultHandler(resolvers).handleResult(this.exchange, result).block(Duration.ofSeconds(5));
|
||||
resultHandler(resolvers).handleResult(this.exchange, result).block(Duration.ofSeconds(5));
|
||||
assertResponseBody(responseBody);
|
||||
}
|
||||
|
||||
private void assertResponseBody(String responseBody) {
|
||||
StepVerifier.create(this.response.getBody())
|
||||
.consumeNextWith(buf -> assertEquals(responseBody,
|
||||
DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8)))
|
||||
.consumeNextWith(buf -> assertEquals(responseBody, dumpString(buf, UTF_8)))
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
|
|
@ -361,15 +393,14 @@ public class ViewResolutionResultHandlerTests {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> render(Map<String, ?> model, MediaType mediaType,
|
||||
ServerWebExchange exchange) {
|
||||
String value = this.name + ": " + model.toString();
|
||||
assertNotNull(value);
|
||||
public Mono<Void> render(Map<String, ?> model, MediaType mediaType, ServerWebExchange exchange) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
if (mediaType != null) {
|
||||
response.getHeaders().setContentType(mediaType);
|
||||
}
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(StandardCharsets.UTF_8));
|
||||
model = new TreeMap<>(model);
|
||||
String value = this.name + ": " + model.toString();
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(UTF_8));
|
||||
DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap(byteBuffer);
|
||||
return response.writeWith(Flux.just(dataBuffer));
|
||||
}
|
||||
|
|
@ -412,6 +443,8 @@ public class ViewResolutionResultHandlerTests {
|
|||
|
||||
Mono<Void> monoVoid() { return null; }
|
||||
|
||||
void voidMethod() {}
|
||||
|
||||
Single<String> singleString() { return null; }
|
||||
|
||||
Single<View> singleView() { return null; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,288 @@
|
|||
/*
|
||||
* 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.beans.PropertyEditor;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.PropertyEditorRegistry;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
|
||||
/**
|
||||
* A specialization of {@link ServerWebInputException} thrown when after data
|
||||
* binding and validation failure. Implements {@link BindingResult} (and its
|
||||
* super-interface {@link Errors}) to allow for direct analysis of binding and
|
||||
* validation errors.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class WebExchangeBindException extends ServerWebInputException implements BindingResult {
|
||||
|
||||
private final BindingResult bindingResult;
|
||||
|
||||
|
||||
public WebExchangeBindException(MethodParameter parameter, BindingResult bindingResult) {
|
||||
super("Validation failure", parameter);
|
||||
this.bindingResult = bindingResult;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the BindingResult that this BindException wraps.
|
||||
* Will typically be a BeanPropertyBindingResult.
|
||||
* @see BeanPropertyBindingResult
|
||||
*/
|
||||
public final BindingResult getBindingResult() {
|
||||
return this.bindingResult;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getObjectName() {
|
||||
return this.bindingResult.getObjectName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNestedPath(String nestedPath) {
|
||||
this.bindingResult.setNestedPath(nestedPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNestedPath() {
|
||||
return this.bindingResult.getNestedPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pushNestedPath(String subPath) {
|
||||
this.bindingResult.pushNestedPath(subPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void popNestedPath() throws IllegalStateException {
|
||||
this.bindingResult.popNestedPath();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void reject(String errorCode) {
|
||||
this.bindingResult.reject(errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String errorCode, String defaultMessage) {
|
||||
this.bindingResult.reject(errorCode, defaultMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String errorCode, Object[] errorArgs, String defaultMessage) {
|
||||
this.bindingResult.reject(errorCode, errorArgs, defaultMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rejectValue(String field, String errorCode) {
|
||||
this.bindingResult.rejectValue(field, errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rejectValue(String field, String errorCode, String defaultMessage) {
|
||||
this.bindingResult.rejectValue(field, errorCode, defaultMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) {
|
||||
this.bindingResult.rejectValue(field, errorCode, errorArgs, defaultMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAllErrors(Errors errors) {
|
||||
this.bindingResult.addAllErrors(errors);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean hasErrors() {
|
||||
return this.bindingResult.hasErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getErrorCount() {
|
||||
return this.bindingResult.getErrorCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ObjectError> getAllErrors() {
|
||||
return this.bindingResult.getAllErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasGlobalErrors() {
|
||||
return this.bindingResult.hasGlobalErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGlobalErrorCount() {
|
||||
return this.bindingResult.getGlobalErrorCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ObjectError> getGlobalErrors() {
|
||||
return this.bindingResult.getGlobalErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectError getGlobalError() {
|
||||
return this.bindingResult.getGlobalError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFieldErrors() {
|
||||
return this.bindingResult.hasFieldErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFieldErrorCount() {
|
||||
return this.bindingResult.getFieldErrorCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FieldError> getFieldErrors() {
|
||||
return this.bindingResult.getFieldErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FieldError getFieldError() {
|
||||
return this.bindingResult.getFieldError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFieldErrors(String field) {
|
||||
return this.bindingResult.hasFieldErrors(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFieldErrorCount(String field) {
|
||||
return this.bindingResult.getFieldErrorCount(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FieldError> getFieldErrors(String field) {
|
||||
return this.bindingResult.getFieldErrors(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FieldError getFieldError(String field) {
|
||||
return this.bindingResult.getFieldError(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getFieldValue(String field) {
|
||||
return this.bindingResult.getFieldValue(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getFieldType(String field) {
|
||||
return this.bindingResult.getFieldType(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getTarget() {
|
||||
return this.bindingResult.getTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getModel() {
|
||||
return this.bindingResult.getModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getRawFieldValue(String field) {
|
||||
return this.bindingResult.getRawFieldValue(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("rawtypes")
|
||||
public PropertyEditor findEditor(String field, Class valueType) {
|
||||
return this.bindingResult.findEditor(field, valueType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyEditorRegistry getPropertyEditorRegistry() {
|
||||
return this.bindingResult.getPropertyEditorRegistry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addError(ObjectError error) {
|
||||
this.bindingResult.addError(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] resolveMessageCodes(String errorCode) {
|
||||
return this.bindingResult.resolveMessageCodes(errorCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] resolveMessageCodes(String errorCode, String field) {
|
||||
return this.bindingResult.resolveMessageCodes(errorCode, field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordSuppressedField(String field) {
|
||||
this.bindingResult.recordSuppressedField(field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSuppressedFields() {
|
||||
return this.bindingResult.getSuppressedFields();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns diagnostic information about the errors held in this object.
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
public String getMessage() {
|
||||
MethodParameter parameter = getMethodParameter().get();
|
||||
StringBuilder sb = new StringBuilder("Validation failed for argument at index ")
|
||||
.append(parameter.getParameterIndex()).append(" in method: ")
|
||||
.append(parameter.getMethod().toGenericString())
|
||||
.append(", with ").append(this.bindingResult.getErrorCount()).append(" error(s): ");
|
||||
for (ObjectError error : this.bindingResult.getAllErrors()) {
|
||||
sb.append("[").append(error).append("] ");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return (this == other || this.bindingResult.equals(other));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.bindingResult.hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue