From 1cdbcc58f329f2ee46e7bb063353a9f737eb84d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 13:26:53 +0100 Subject: [PATCH] Add AssertJ support for the Model This commit adds AssertJ compatible assertions for the Model that is generated from an HTTP request. See gh-21178 --- .../AbstractBindingResultAssert.java | 123 ++++++++++++ .../test/validation/package-info.java | 9 + .../test/web/servlet/assertj/ModelAssert.java | 163 ++++++++++++++++ .../AbstractBindingResultAssertTests.java | 136 ++++++++++++++ .../web/servlet/assertj/ModelAssertTests.java | 176 ++++++++++++++++++ 5 files changed, 607 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/validation/package-info.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java create mode 100644 spring-test/src/test/java/org/springframework/test/validation/AbstractBindingResultAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java diff --git a/spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java b/spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java new file mode 100644 index 00000000000..e6acd9523e6 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.validation; + +import java.util.List; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ListAssert; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * {@link BindingResult}. + * + * @author Stephane Nicoll + * @since 6.2 + * @param the type of assertions + */ +public abstract class AbstractBindingResultAssert> extends AbstractAssert { + + private final Failures failures = Failures.instance(); + + private final String name; + + protected AbstractBindingResultAssert(String name, BindingResult bindingResult, Class selfType) { + super(bindingResult, selfType); + this.name = name; + as("Binding result for attribute '%s", this.name); + } + + /** + * Verify that the total number of errors is equal to the given one. + * @param expected the expected number of errors + */ + public SELF hasErrorsCount(int expected) { + assertThat(this.actual.getErrorCount()) + .as("check errors for attribute '%s'", this.name).isEqualTo(expected); + return this.myself; + } + + /** + * Verify that the actual binding result contains fields in error with the + * given {@code fieldNames}. + * @param fieldNames the names of fields that should be in error + */ + public SELF hasFieldErrors(String... fieldNames) { + assertThat(fieldErrorNames()).contains(fieldNames); + return this.myself; + } + + /** + * Verify that the actual binding result contains only fields in + * error with the given {@code fieldNames}, and nothing else. + * @param fieldNames the exhaustive list of field name that should be in error + */ + public SELF hasOnlyFieldErrors(String... fieldNames) { + assertThat(fieldErrorNames()).containsOnly(fieldNames); + return this.myself; + } + + /** + * Verify that the field with the given {@code fieldName} has an error + * matching the given {@code errorCode}. + * @param fieldName the name of a field in error + * @param errorCode the error code for that field + */ + public SELF hasFieldErrorCode(String fieldName, String errorCode) { + Assertions.assertThat(getFieldError(fieldName).getCode()) + .as("check error code for field '%s'", fieldName).isEqualTo(errorCode); + return this.myself; + } + + protected AssertionError unexpectedBindingResult(String reason, Object... arguments) { + return this.failures.failure(this.info, new UnexpectedBindingResult(reason, arguments)); + } + + private AssertProvider> fieldErrorNames() { + return () -> { + List actual = this.actual.getFieldErrors().stream().map(FieldError::getField).toList(); + return new ListAssert<>(actual).as("check field errors"); + }; + } + + private FieldError getFieldError(String fieldName) { + FieldError fieldError = this.actual.getFieldError(fieldName); + if (fieldError == null) { + throw unexpectedBindingResult("to have at least an error for field '%s'", fieldName); + } + return fieldError; + } + + + private final class UnexpectedBindingResult extends BasicErrorMessageFactory { + + private UnexpectedBindingResult(String reason, Object... arguments) { + super("%nExpecting binding result:%n %s%n%s", AbstractBindingResultAssert.this.actual, + reason.formatted(arguments)); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/validation/package-info.java b/spring-test/src/main/java/org/springframework/test/validation/package-info.java new file mode 100644 index 00000000000..caa3fdcadda --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/validation/package-info.java @@ -0,0 +1,9 @@ +/** + * Testing support for validation. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.validation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java new file mode 100644 index 00000000000..b7bd5109855 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.web.servlet.assertj; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import org.assertj.core.api.AbstractMapAssert; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import org.springframework.lang.Nullable; +import org.springframework.test.validation.AbstractBindingResultAssert; +import org.springframework.validation.BindingResult; +import org.springframework.validation.BindingResultUtils; +import org.springframework.validation.Errors; +import org.springframework.web.servlet.ModelAndView; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * a {@linkplain ModelAndView#getModel() model}. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class ModelAssert extends AbstractMapAssert, String, Object> { + + private final Failures failures = Failures.instance(); + + public ModelAssert(Map map) { + super(map, ModelAssert.class); + } + + /** + * Return a new {@linkplain AbstractBindingResultAssert assertion} object + * that uses the {@link BindingResult} with the given {@code name} as the + * object to test. + * Examples:

+	 * // Check that the "person" attribute in the model has 2 errors:
+	 * assertThat(...).model().extractingBindingResult("person").hasErrorsCount(2);
+	 * 
+ */ + public AbstractBindingResultAssert extractingBindingResult(String name) { + BindingResult result = BindingResultUtils.getBindingResult(this.actual, name); + if (result == null) { + throw unexpectedModel("to have a binding result for attribute '%s'", name); + } + return new BindingResultAssert(name, result); + } + + /** + * Verify that the actual model has at least one error. + */ + public ModelAssert hasErrors() { + if (getAllErrors() == 0) { + throw unexpectedModel("to have at least one error"); + } + return this.myself; + } + + /** + * Verify that the actual model does not have any errors. + */ + public ModelAssert doesNotHaveErrors() { + int count = getAllErrors(); if (count > 0) { + throw unexpectedModel("to not have an error, but got %s", count); + } + return this.myself; + } + + /** + * Verify that the actual model contain the attributes with the given + * {@code names}, and that these attributes have each at least one error. + * @param names the expected names of attributes with errors + */ + public ModelAssert hasAttributeErrors(String... names) { + return assertAttributes(names, BindingResult::hasErrors, + "to have attribute errors for", "these attributes do not have any error"); + } + + /** + * Verify that the actual model contain the attributes with the given + * {@code names}, and that these attributes do not have any error. + * @param names the expected names of attributes without errors + */ + public ModelAssert doesNotHaveAttributeErrors(String... names) { + return assertAttributes(names, Predicate.not(BindingResult::hasErrors), + "to have attribute without errors for", "these attributes have at least an error"); + } + + private ModelAssert assertAttributes(String[] names, Predicate condition, + String assertionMessage, String failAssertionMessage) { + + Set missing = new LinkedHashSet<>(); + Set failCondition = new LinkedHashSet<>(); + for (String name : names) { + BindingResult bindingResult = getBindingResult(name); + if (bindingResult == null) { + missing.add(name); + } + else if (!condition.test(bindingResult)) { + failCondition.add(name); + } + } + if (!missing.isEmpty() || !failCondition.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("%n%s:%n %s%n".formatted(assertionMessage, String.join(", ", names))); + if (!missing.isEmpty()) { + sb.append("%nbut could not find these attributes:%n %s%n".formatted(String.join(", ", missing))); + } + if (!failCondition.isEmpty()) { + String prefix = missing.isEmpty() ? "but" : "and"; + sb.append("%n%s %s:%n %s%n".formatted(prefix, failAssertionMessage, String.join(", ", failCondition))); + } + throw unexpectedModel(sb.toString()); + } + return this.myself; + } + + private AssertionError unexpectedModel(String reason, Object... arguments) { + return this.failures.failure(this.info, new UnexpectedModel(reason, arguments)); + } + + private int getAllErrors() { + return this.actual.values().stream().filter(Errors.class::isInstance).map(Errors.class::cast) + .map(Errors::getErrorCount).reduce(0, Integer::sum); + } + + @Nullable + private BindingResult getBindingResult(String name) { + return BindingResultUtils.getBindingResult(this.actual, name); + } + + private final class UnexpectedModel extends BasicErrorMessageFactory { + + private UnexpectedModel(String reason, Object... arguments) { + super("%nExpecting model:%n %s%n%s", ModelAssert.this.actual, reason.formatted(arguments)); + } + } + + private static final class BindingResultAssert extends AbstractBindingResultAssert { + public BindingResultAssert(String name, BindingResult bindingResult) { + super(name, bindingResult, BindingResultAssert.class); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/validation/AbstractBindingResultAssertTests.java b/spring-test/src/test/java/org/springframework/test/validation/AbstractBindingResultAssertTests.java new file mode 100644 index 00000000000..39ee91e2349 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/validation/AbstractBindingResultAssertTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.validation; + +import java.util.Map; + +import org.assertj.core.api.AssertProvider; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.DataBinder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AbstractBindingResultAssert}. + * + * @author Stephane Nicoll + */ +class AbstractBindingResultAssertTests { + + @Test + void hasErrorsCountWithNoError() { + assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "42"))).hasErrorsCount(0); + } + + @Test + void hasErrorsCountWithInvalidCount() { + AssertProvider actual = bindingResult(new TestBean(), + Map.of("name", "John", "age", "4x", "touchy", "invalid.value")); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasErrorsCount(1)) + .withMessageContainingAll("check errors for attribute 'test'", "1", "2"); + } + + @Test + void hasFieldErrorsWithMatchingSubset() { + assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"))) + .hasFieldErrors("touchy"); + } + + @Test + void hasFieldErrorsWithAllMatching() { + assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"))) + .hasFieldErrors("touchy", "age"); + } + + @Test + void hasFieldErrorsWithNotAllMatching() { + AssertProvider actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasFieldErrors("age", "name")) + .withMessageContainingAll("check field errors", "age", "touchy", "name"); + } + + @Test + void hasOnlyFieldErrorsWithAllMatching() { + assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"))) + .hasOnlyFieldErrors("touchy", "age"); + } + + @Test + void hasOnlyFieldErrorsWithMatchingSubset() { + AssertProvider actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasOnlyFieldErrors("age")) + .withMessageContainingAll("check field errors", "age", "touchy"); + } + + @Test + void hasFieldErrorCodeWithMatchingCode() { + assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"))) + .hasFieldErrorCode("age", "typeMismatch"); + } + + @Test + void hasFieldErrorCodeWitNonMatchingCode() { + AssertProvider actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasFieldErrorCode("age", "castFailure")) + .withMessageContainingAll("check error code for field 'age'", "castFailure", "typeMismatch"); + } + + @Test + void hasFieldErrorCodeWitNonMatchingField() { + AssertProvider actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasFieldErrorCode("unknown", "whatever")) + .withMessageContainingAll("Expecting binding result", "touchy", "age", + "to have at least an error for field 'unknown'"); + } + + + private AssertProvider bindingResult(Object instance, Map propertyValues) { + return () -> new BindingResultAssert("test", createBindingResult(instance, propertyValues)); + } + + private static BindingResult createBindingResult(Object instance, Map propertyValues) { + DataBinder binder = new DataBinder(instance, "test"); + MutablePropertyValues pvs = new MutablePropertyValues(propertyValues); + binder.bind(pvs); + try { + binder.close(); + return binder.getBindingResult(); + } + catch (BindException ex) { + return ex.getBindingResult(); + } + } + + + private static final class BindingResultAssert extends AbstractBindingResultAssert { + public BindingResultAssert(String name, BindingResult bindingResult) { + super(name, bindingResult, BindingResultAssert.class); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java new file mode 100644 index 00000000000..7126fdf3495 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.web.servlet.assertj; + +import java.util.HashMap; +import java.util.Map; + +import org.assertj.core.api.AssertProvider; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.validation.BindException; +import org.springframework.validation.DataBinder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link ModelAssert}. + * + * @author Stephane Nicoll + */ +class ModelAssertTests { + + @Test + void hasErrors() { + assertThat(forModel(new TestBean(), Map.of("name", "John", "age", "4x"))).hasErrors(); + } + + @Test + void hasErrorsWithNoError() { + AssertProvider actual = forModel(new TestBean(), Map.of("name", "John", "age", "42")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(actual).hasErrors()) + .withMessageContainingAll("John", "to have at least one error"); + } + + @Test + void doesNotHaveErrors() { + assertThat(forModel(new TestBean(), Map.of("name", "John", "age", "42"))).doesNotHaveErrors(); + } + + @Test + void doesNotHaveErrorsWithError() { + AssertProvider actual = forModel(new TestBean(), Map.of("name", "John", "age", "4x")); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(actual).doesNotHaveErrors()) + .withMessageContainingAll("John", "to not have an error, but got 1"); + } + + @Test + void extractBindingResultForAttributeInError() { + Map model = new HashMap<>(); + augmentModel(model, "person", new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "invalid.value")); + assertThat(forModel(model)).extractingBindingResult("person").hasErrorsCount(2); + } + + @Test + void hasErrorCountForUnknownAttribute() { + Map model = new HashMap<>(); + augmentModel(model, "person", new TestBean(), Map.of("name", "John", "age", "42")); + AssertProvider actual = forModel(model); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).extractingBindingResult("user")) + .withMessageContainingAll("to have a binding result for attribute 'user'"); + } + + @Test + void hasErrorsWithMatchingAttributes() { + Map model = new HashMap<>(); + augmentModel(model, "wrong1", new TestBean(), Map.of("name", "first", "age", "4x")); + augmentModel(model, "valid", new TestBean(), Map.of("name", "second")); + augmentModel(model, "wrong2", new TestBean(), Map.of("name", "third", "touchy", "invalid.name")); + assertThat(forModel(model)).hasAttributeErrors("wrong1", "wrong2"); + } + + @Test + void hasErrorsWithOneNonMatchingAttribute() { + Map model = new HashMap<>(); + augmentModel(model, "wrong1", new TestBean(), Map.of("name", "first", "age", "4x")); + augmentModel(model, "valid", new TestBean(), Map.of("name", "second")); + augmentModel(model, "wrong2", new TestBean(), Map.of("name", "third", "touchy", "invalid.name")); + AssertProvider actual = forModel(model); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasAttributeErrors("wrong1", "valid")) + .withMessageContainingAll("to have attribute errors for:", "wrong1, valid", + "but these attributes do not have any error:", "valid"); + } + + @Test + void hasErrorsWithOneNonMatchingAttributeAndOneUnknownAttribute() { + Map model = new HashMap<>(); + augmentModel(model, "wrong1", new TestBean(), Map.of("name", "first", "age", "4x")); + augmentModel(model, "valid", new TestBean(), Map.of("name", "second")); + augmentModel(model, "wrong2", new TestBean(), Map.of("name", "third", "touchy", "invalid.name")); + AssertProvider actual = forModel(model); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).hasAttributeErrors("wrong1", "unknown", "valid")) + .withMessageContainingAll("to have attribute errors for:", "wrong1, unknown, valid", + "but could not find these attributes:", "unknown", + "and these attributes do not have any error:", "valid"); + } + + @Test + void doesNotHaveErrorsWithMatchingAttributes() { + Map model = new HashMap<>(); + augmentModel(model, "valid1", new TestBean(), Map.of("name", "first")); + augmentModel(model, "wrong", new TestBean(), Map.of("name", "second", "age", "4x")); + augmentModel(model, "valid2", new TestBean(), Map.of("name", "third")); + assertThat(forModel(model)).doesNotHaveAttributeErrors("valid1", "valid2"); + } + + @Test + void doesNotHaveErrorsWithOneNonMatchingAttribute() { + Map model = new HashMap<>(); + augmentModel(model, "valid1", new TestBean(), Map.of("name", "first")); + augmentModel(model, "wrong", new TestBean(), Map.of("name", "second", "age", "4x")); + augmentModel(model, "valid2", new TestBean(), Map.of("name", "third")); + AssertProvider actual = forModel(model); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).doesNotHaveAttributeErrors("valid1", "wrong")) + .withMessageContainingAll("to have attribute without errors for:", "valid1, wrong", + "but these attributes have at least an error:", "wrong"); + } + + @Test + void doesNotHaveErrorsWithOneNonMatchingAttributeAndOneUnknownAttribute() { + Map model = new HashMap<>(); + augmentModel(model, "valid1", new TestBean(), Map.of("name", "first")); + augmentModel(model, "wrong", new TestBean(), Map.of("name", "second", "age", "4x")); + augmentModel(model, "valid2", new TestBean(), Map.of("name", "third")); + AssertProvider actual = forModel(model); + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(actual).doesNotHaveAttributeErrors("valid1", "unknown", "wrong")) + .withMessageContainingAll("to have attribute without errors for:", "valid1, unknown, wrong", + "but could not find these attributes:", "unknown", + "and these attributes have at least an error:", "wrong"); + } + + private AssertProvider forModel(Map model) { + return () -> new ModelAssert(model); + } + + private AssertProvider forModel(Object instance, Map propertyValues) { + Map model = new HashMap<>(); + augmentModel(model, "test", instance, propertyValues); + return forModel(model); + } + + private static void augmentModel(Map model, String attribute, Object instance, Map propertyValues) { + DataBinder binder = new DataBinder(instance, attribute); + MutablePropertyValues pvs = new MutablePropertyValues(propertyValues); + binder.bind(pvs); + try { + binder.close(); + model.putAll(binder.getBindingResult().getModel()); + } + catch (BindException ex) { + model.putAll(ex.getBindingResult().getModel()); + } + } + +}