diff --git a/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java
index eac45811dd3..201160c86c5 100644
--- a/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java
+++ b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java
@@ -54,10 +54,10 @@ public class BindingAwareConcurrentModel extends ConcurrentModel {
private void removeBindingResultIfNecessary(String key, Object value) {
if (!key.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
- String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key;
- BindingResult bindingResult = (BindingResult) get(bindingResultKey);
- if (bindingResult != null && bindingResult.getTarget() != value) {
- remove(bindingResultKey);
+ String resultKey = BindingResult.MODEL_KEY_PREFIX + key;
+ BindingResult result = (BindingResult) get(resultKey);
+ if (result != null && result.getTarget() != value) {
+ remove(resultKey);
}
}
}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java
index 5bb4b236a96..58414125575 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java
@@ -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());
}
}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java
new file mode 100644
index 00000000000..2598937f175
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java
@@ -0,0 +1,201 @@
+/*
+ * 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.
+ *
+ *
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}.
+ *
+ *
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 boolean useDefaultResolution;
+
+ private final ReactiveAdapterRegistry adapterRegistry;
+
+
+ /**
+ * Class constructor.
+ * @param useDefaultResolution if "true", non-simple method arguments and
+ * return values are considered model attributes with or without a
+ * {@code @ModelAttribute} annotation present.
+ * @param registry for adapting to other reactive types from and to Mono
+ */
+ public ModelAttributeMethodArgumentResolver(boolean useDefaultResolution,
+ ReactiveAdapterRegistry registry) {
+
+ 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 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 model = context.getModel().asMap();
+ MonoProcessor 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 ann = parameter.getParameterAnnotation(ModelAttribute.class);
+ String name = (ann != null ? ann.value() : null);
+ // TODO: Conventions does not deal with async wrappers
+ return StringUtils.hasText(name) ? name : 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);
+ }
+ }
+ }
+
+}
diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java
new file mode 100644
index 00000000000..5efef3e3fc8
--- /dev/null
+++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolverTests.java
@@ -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 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(false, new ReactiveAdapterRegistry());
+
+ 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(true, new ReactiveAdapterRegistry());
+
+ 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 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 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>> 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(false, 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 mono,
+ @ModelAttribute @Validated Single single,
+ Foo fooNotAnnotated,
+ String stringNotAnnotated,
+ Mono monoNotAnnotated,
+ Mono monoStringNotAnnotated) {}
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/WebExchangeBindException.java b/spring-web/src/main/java/org/springframework/web/bind/WebExchangeBindException.java
new file mode 100644
index 00000000000..99182fa8850
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/bind/WebExchangeBindException.java
@@ -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 getAllErrors() {
+ return this.bindingResult.getAllErrors();
+ }
+
+ @Override
+ public boolean hasGlobalErrors() {
+ return this.bindingResult.hasGlobalErrors();
+ }
+
+ @Override
+ public int getGlobalErrorCount() {
+ return this.bindingResult.getGlobalErrorCount();
+ }
+
+ @Override
+ public List 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 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 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 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();
+ }
+
+}