diff options
author | Denis Anisimov <denis@vaadin.com> | 2016-08-25 14:53:09 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2016-09-02 15:13:19 +0300 |
commit | ccaabe6db025f7e73adc83b4d0e2671c7fa16d40 (patch) | |
tree | d1508def2ee24cac623616c1b4fdd29a6b8f069c /server/src | |
parent | 876b6383e6ec50a8bbe34126b7bfed5f6f616bea (diff) | |
download | vaadin-framework-ccaabe6db025f7e73adc83b4d0e2671c7fa16d40.tar.gz vaadin-framework-ccaabe6db025f7e73adc83b4d0e2671c7fa16d40.zip |
Add item level validator support to Binder
An item level validator is run on the item (bean) after field validators
have passed. A failed item level validator will block save operations,
just like field level validators.
Change-Id: I3b918b33371ceef07cdfbd0a8b6d477d4ac26b85
Diffstat (limited to 'server/src')
6 files changed, 557 insertions, 84 deletions
diff --git a/server/src/main/java/com/vaadin/data/BeanBinder.java b/server/src/main/java/com/vaadin/data/BeanBinder.java index 18985bc90c..63ad9ef04f 100644 --- a/server/src/main/java/com/vaadin/data/BeanBinder.java +++ b/server/src/main/java/com/vaadin/data/BeanBinder.java @@ -311,6 +311,11 @@ public class BeanBinder<BEAN> extends Binder<BEAN> { } @Override + public BeanBinder<BEAN> withValidator(Validator<? super BEAN> validator) { + return (BeanBinder<BEAN>) super.withValidator(validator); + } + + @Override protected <FIELDVALUE, TARGET> BeanBindingImpl<BEAN, FIELDVALUE, TARGET> createBinding( HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converter, StatusChangeHandler handler) { @@ -318,4 +323,5 @@ public class BeanBinder<BEAN> extends Binder<BEAN> { Objects.requireNonNull(converter, "converter cannot be null"); return new BeanBindingImpl<>(this, field, converter, handler); } + } diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index 094afd2bd8..7fb1e47ed7 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -17,15 +17,19 @@ package com.vaadin.data; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.StringToIntegerConverter; @@ -42,13 +46,25 @@ import com.vaadin.ui.Label; * explicit logic needed to move data between the UI and the data layers of the * application. * <p> - * A binder is a collection of <i>bindings</i>, each representing the - * association of a single field and a backing property. + * A binder is a collection of <i>bindings</i>, each representing the mapping of + * a single field, through converters and validators, to a backing property. * <p> * A binder instance can be bound to a single bean instance at a time, but can * be rebound as needed. This allows usage patterns like a <i>master-details</i> * view, where a select component is used to pick the bean to edit. * <p> + * Bean level validators can be added using the + * {@link #withValidator(Validator)} method and will be run on the bound bean + * once it has been updated from the values of the bound fields. Bean level + * validators are also run as part of {@link #save(Object)} and + * {@link #saveIfValid(Object)} if all field level validators pass. + * <p> + * Note: For bean level validators, the item must be updated before the + * validators are run. If a bean level validator fails in {@link #save(Object)} + * or {@link #saveIfValid(Object)}, the item will be reverted to the previous + * state before returning from the method. You should ensure that the + * getters/setters in the item do not have side effects. + * <p> * Unless otherwise specified, {@code Binder} method arguments cannot be null. * * @author Vaadin Ltd. @@ -484,14 +500,12 @@ public class Binder<BEAN> implements Serializable { private void bind(BEAN bean) { setFieldValue(bean); onValueChange = getField() - .addValueChangeListener(e -> storeFieldValue(bean)); + .addValueChangeListener(e -> storeFieldValue(bean, true)); } @Override public Result<TARGET> validate() { - FIELDVALUE fieldValue = field.getValue(); - Result<TARGET> dataValue = converterValidatorChain.convertToModel( - fieldValue, ((AbstractComponent) field).getLocale()); + Result<TARGET> dataValue = getTargetValue(); fireStatusChangeEvent(dataValue); return dataValue; } @@ -503,7 +517,10 @@ public class Binder<BEAN> implements Serializable { * describing an error */ private Result<TARGET> getTargetValue() { - return validate(); + FIELDVALUE fieldValue = field.getValue(); + Result<TARGET> dataValue = converterValidatorChain.convertToModel( + fieldValue, ((AbstractComponent) field).getLocale()); + return dataValue; } private void unbind() { @@ -530,18 +547,35 @@ public class Binder<BEAN> implements Serializable { /** * Saves the field value by invoking the setter function on the given - * bean, if the value passes all registered validators. + * bean, if the value passes all registered validators. Optionally runs + * item level validators if all field validators pass. * * @param bean * the bean to set the property value to + * @param runBeanLevelValidation + * <code>true</code> to run item level validators if all + * field validators pass, <code>false</code> to always skip + * item level validators */ - private void storeFieldValue(BEAN bean) { + private void storeFieldValue(BEAN bean, + boolean runBeanLevelValidation) { assert bean != null; if (setter != null) { - getTargetValue().ifOk(value -> setter.accept(bean, value)); + getTargetValue().ifOk(value -> setBeanValue(bean, value)); + } + if (runBeanLevelValidation && !getBinder().bindings.stream() + .map(BindingImpl::getTargetValue) + .anyMatch(Result::isError)) { + List<ValidationError<?>> errors = binder.validateItem(bean); + // TODO: Pass errors to Binder statusChangeHandler once that is + // available } } + private void setBeanValue(BEAN bean, TARGET value) { + setter.accept(bean, value); + } + private void fireStatusChangeEvent(Result<TARGET> result) { ValidationStatusChangeEvent event = new ValidationStatusChangeEvent( getField(), @@ -594,7 +628,9 @@ public class Binder<BEAN> implements Serializable { private BEAN bean; - private Set<BindingImpl<BEAN, ?, ?>> bindings = new LinkedHashSet<>(); + private final Set<BindingImpl<BEAN, ?, ?>> bindings = new LinkedHashSet<>(); + + private final List<Validator<? super BEAN>> validators = new ArrayList<>(); /** * Returns an {@code Optional} of the bean that has been bound with @@ -683,6 +719,14 @@ public class Binder<BEAN> implements Serializable { * corresponding getter functions. Any changes to field values are reflected * back to their corresponding property values of the bean as long as the * bean is bound. + * <p> + * Any change made in the fields also runs validation for the field + * {@link Binding} and bean level validation for this binder (bean level + * validators are added using {@link Binder#withValidator(Validator)}. + * + * @see #load(Object) + * @see #save(Object) + * @see #saveIfValid(Object) * * @param bean * the bean to edit, not null @@ -695,24 +739,6 @@ public class Binder<BEAN> implements Serializable { } /** - * Validates the values of all bound fields and returns the result of the - * validation as a set of validation errors. - * <p> - * Validation is successful if the resulting set is empty. - * - * @return the validation result. - */ - public List<ValidationError<?>> validate() { - - List<ValidationError<?>> resultErrors = new ArrayList<>(); - for (BindingImpl<?, ?, ?> binding : bindings) { - binding.validate().ifError(errorMessage -> resultErrors.add( - new ValidationError<>(binding.getField(), errorMessage))); - } - return resultErrors; - } - - /** * Unbinds the currently bound bean if any. If there is no bound bean, does * nothing. */ @@ -725,9 +751,15 @@ public class Binder<BEAN> implements Serializable { /** * Reads the bound property values from the given bean to the corresponding - * fields. The bean is not otherwise associated with this binder; in - * particular its property values are not bound to the field value changes. - * To achieve that, use {@link #bind(BEAN)}. + * fields. + * <p> + * The bean is not otherwise associated with this binder; in particular its + * property values are not bound to the field value changes. To achieve + * that, use {@link #bind(BEAN)}. + * + * @see #bind(Object) + * @see #saveIfValid(Object) + * @see #save(Object) * * @param bean * the bean whose property values to read, not null @@ -739,10 +771,15 @@ public class Binder<BEAN> implements Serializable { /** * Saves changes from the bound fields to the given bean if all validators - * pass. + * (binding and bean level) pass. + * <p> + * If any field binding validator fails, no values are saved and a + * {@code ValidationException} is thrown. * <p> - * If any field binding validator fails, no values are saved and an - * exception is thrown. + * If all field level validators pass, the given bean is updated and bean + * level validators are run on the updated item. If any bean level validator + * fails, the bean updates are reverted and a {@code ValidationException} is + * thrown. * * @see #saveIfValid(Object) * @see #load(Object) @@ -762,12 +799,16 @@ public class Binder<BEAN> implements Serializable { /** * Saves changes from the bound fields to the given bean if all validators - * pass. + * (binding and bean level) pass. * <p> * If any field binding validator fails, no values are saved and * <code>false</code> is returned. + * <p> + * If all field level validators pass, the given bean is updated and bean + * level validators are run on the updated item. If any bean level validator + * fails, the bean updates are reverted and <code>false</code> is returned. * - * @see #saveIfValid(Object) + * @see #save(Object) * @see #load(Object) * @see #bind(Object) * @@ -782,21 +823,128 @@ public class Binder<BEAN> implements Serializable { /** * Saves the field values into the given bean if all field level validators - * pass. + * pass. Runs bean level validators on the bean after saving. * * @param bean * the bean to save field values into - * @return a list of field validation errors + * @return a list of field validation errors if such occur, otherwise a list + * of bean validation errors. */ private List<ValidationError<?>> doSaveIfValid(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); // First run fields level validation - List<ValidationError<?>> errors = validate(); + List<ValidationError<?>> errors = validateBindings(); // If no validation errors then update bean - if (errors.isEmpty()) { - bindings.forEach(binding -> binding.storeFieldValue(bean)); + if (!errors.isEmpty()) { + return errors; + } + + // Save old bean values so we can restore them if validators fail + Map<Binding<BEAN, ?, ?>, Object> oldValues = new HashMap<>(); + bindings.forEach(binding -> oldValues.put(binding, + binding.convertDataToFieldType(bean))); + + bindings.forEach(binding -> binding.storeFieldValue(bean, false)); + // Now run bean level validation against the updated bean + List<ValidationError<?>> itemValidatorErrors = validateItem(bean); + if (!itemValidatorErrors.isEmpty()) { + // Item validator failed, revert values + bindings.forEach((BindingImpl binding) -> binding.setBeanValue(bean, + oldValues.get(binding))); + } + return itemValidatorErrors; + } + + /** + * Adds an item level validator. + * <p> + * Item level validators are applied on the item instance after the item is + * updated. If the validators fail, the item instance is reverted to its + * previous state. + * + * @see #save(Object) + * @see #saveIfValid(Object) + * + * @param validator + * the validator to add, not null + * @return this binder, for chaining + */ + public Binder<BEAN> withValidator(Validator<? super BEAN> validator) { + Objects.requireNonNull(validator, "validator cannot be null"); + validators.add(validator); + return this; + } + + /** + * Validates the values of all bound fields and returns the result of the + * validation as a list of validation errors. + * <p> + * If all field level validators pass, and {@link #bind(Object)} has been + * used to bind to an item, item level validators are run for that bean. + * Item level validators are ignored if there is no bound item or if any + * field level validator fails. + * <p> + * Validation is successful if the returned list is empty. + * + * @return a list of validation errors or an empty list if validation + * succeeded + */ + public List<ValidationError<?>> validate() { + List<ValidationError<?>> errors = validateBindings(); + if (!errors.isEmpty()) { + return errors; + } + + if (bean != null) { + return validateItem(bean); + } + + return Collections.emptyList(); + } + + /** + * Validates the bindings and returns the result of the validation as a list + * of validation errors. + * <p> + * If all validators pass, the resulting list is empty. + * <p> + * Does not run bean validators. + * + * @see #validateItem(Object) + * + * @return a list of validation errors or an empty list if validation + * succeeded + */ + private List<ValidationError<?>> validateBindings() { + List<ValidationError<?>> resultErrors = new ArrayList<>(); + for (BindingImpl<?, ?, ?> binding : bindings) { + binding.validate().ifError(errorMessage -> resultErrors + .add(new ValidationError<>(binding, + binding.getField().getValue(), errorMessage))); } - return errors; + return resultErrors; + } + + /** + * Validates the {@code item} using item validators added using + * {@link #withValidator(Validator)} and returns the result of the + * validation as a list of validation errors. + * <p> + * If all validators pass, the resulting list is empty. + * + * @see #withValidator(Validator) + * + * @param bean + * the bean to validate + * @return a list of validation errors or an empty list if validation + * succeeded + */ + private List<ValidationError<?>> validateItem(BEAN bean) { + Objects.requireNonNull(bean, "bean cannot be null"); + return validators.stream().map(validator -> validator.apply(bean)) + .filter(Result::isError).map(res -> new ValidationError<>(this, + bean, res.getMessage().get())) + .collect(Collectors.toList()); } /** diff --git a/server/src/main/java/com/vaadin/data/ValidationError.java b/server/src/main/java/com/vaadin/data/ValidationError.java index 32d9b71c31..1555ecad2c 100644 --- a/server/src/main/java/com/vaadin/data/ValidationError.java +++ b/server/src/main/java/com/vaadin/data/ValidationError.java @@ -17,44 +17,67 @@ package com.vaadin.data; import java.io.Serializable; import java.util.Objects; +import java.util.Optional; + +import com.vaadin.data.Binder.Binding; /** - * Represents a validation error. An error contains a reference to a field whose - * value is invalid and a message describing a validation failure. + * Represents a validation error. + * <p> + * A validation error is either connected to a field validator ( + * {@link #getField()} returns a non-empty optional) or to an item level + * validator ({@link #getField()} returns an empty optional). * * @author Vaadin Ltd * @since 8.0 * * @param <V> - * the field value type + * the value type */ public class ValidationError<V> implements Serializable { - private HasValue<V> field; - private String message; + /** + * This is either a {@link Binding} or a {@link Binder}. + */ + private final Object source; + private final String message; + /** + * This is either HasValue<V> value (in case of Binding) or bean (in case of + * Binder). + */ + private final V value; /** - * Creates a new instance of ValidationError with provided validated field - * and error message. + * Creates a new instance of ValidationError using the provided source + * ({@link Binding} or {@link Binder}), value and error message. * - * @param field - * the validated field + * @param source + * the validated binding or the binder + * @param value + * the invalid value * @param message * the validation error message, not {@code null} */ - public ValidationError(HasValue<V> field, String message) { + public ValidationError(Object source, V value, String message) { Objects.requireNonNull(message, "message cannot be null"); - this.field = field; + this.source = source; this.message = message; + this.value = value; } /** - * Returns a reference to the validated field. + * Returns a reference to the validated field or an empty optional if the + * validation was not related to a single field. * - * @return the validated field + * @return the validated field or an empty optional */ - public HasValue<V> getField() { - return field; + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Optional<HasValue<V>> getField() { + if (source instanceof Binding) { + return Optional.of(((Binding) source).getField()); + } else { + return Optional.empty(); + } } /** @@ -65,4 +88,17 @@ public class ValidationError<V> implements Serializable { public String getMessage() { return message; } + + /** + * Returns the invalid value. + * <p> + * This is either the field value (if the validator error comes from a field + * binding) or the bean (for item validators). + * + * @return the source value + */ + public V getValue() { + return value; + } + } diff --git a/server/src/test/java/com/vaadin/data/BeanBinderTest.java b/server/src/test/java/com/vaadin/data/BeanBinderTest.java index 6b535c53a6..1788c3a9e8 100644 --- a/server/src/test/java/com/vaadin/data/BeanBinderTest.java +++ b/server/src/test/java/com/vaadin/data/BeanBinderTest.java @@ -13,12 +13,12 @@ import com.vaadin.ui.TextField; public class BeanBinderTest { - BeanBinder<BeanToValidate> binder; + private BeanBinder<BeanToValidate> binder; - TextField nameField; - TextField ageField; + private TextField nameField; + private TextField ageField; - BeanToValidate p = new BeanToValidate(); + private BeanToValidate p = new BeanToValidate(); @Before public void setUp() { @@ -165,7 +165,7 @@ public class BeanBinderTest { private void assertInvalid(HasValue<?> field, String message) { List<ValidationError<?>> errors = binder.validate(); assertEquals(1, errors.size()); - assertSame(field, errors.get(0).getField()); + assertSame(field, errors.get(0).getField().get()); assertEquals(message, errors.get(0).getMessage()); } } diff --git a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java index a50e06d10f..6ed2170fa7 100644 --- a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java +++ b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java @@ -28,10 +28,11 @@ import com.vaadin.data.Binder.Binding; import com.vaadin.data.util.converter.StringToIntegerConverter; import com.vaadin.data.validator.EmailValidator; import com.vaadin.server.AbstractErrorMessage; -import com.vaadin.ui.AbstractField; +import com.vaadin.ui.Button; import com.vaadin.ui.Label; import com.vaadin.ui.PopupDateField; import com.vaadin.ui.Slider; +import com.vaadin.ui.TextField; /** * Book of Vaadin tests. @@ -41,25 +42,23 @@ import com.vaadin.ui.Slider; */ public class BinderBookOfVaadinTest { - static class TextField extends AbstractField<String> { - - String value = ""; + private static class BookPerson { + private String lastName; + private String email, phone, title; + private int yearOfBirth, salaryLevel; - @Override - public String getValue() { - return value; + public BookPerson(int yearOfBirth, int salaryLevel) { + this.yearOfBirth = yearOfBirth; + this.salaryLevel = salaryLevel; } - @Override - protected void doSetValue(String value) { - this.value = value; + public BookPerson(BookPerson origin) { + this(origin.yearOfBirth, origin.salaryLevel); + lastName = origin.lastName; + email = origin.email; + phone = origin.phone; + title = origin.title; } - } - - private static class BookPerson { - private String lastName; - private String email; - private int yearOfBirth, salaryLevel; public String getLastName() { return lastName; @@ -69,11 +68,6 @@ public class BinderBookOfVaadinTest { this.lastName = lastName; } - public BookPerson(int yearOfBirth, int salaryLevel) { - this.yearOfBirth = yearOfBirth; - this.salaryLevel = salaryLevel; - } - public int getYearOfBirth() { return yearOfBirth; } @@ -98,6 +92,22 @@ public class BinderBookOfVaadinTest { this.email = email; } + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } public static class Trip { @@ -115,11 +125,15 @@ public class BinderBookOfVaadinTest { private Binder<BookPerson> binder; private TextField field; + private TextField phoneField; + private TextField emailField; @Before public void setUp() { binder = new Binder<>(); field = new TextField(); + phoneField = new TextField(); + emailField = new TextField(); } @Test @@ -421,4 +435,45 @@ public class BinderBookOfVaadinTest { Assert.assertEquals(field, evt.getSource()); } + @Test + public void binder_saveIfValid() { + BeanBinder<BookPerson> binder = new BeanBinder<BookPerson>( + BookPerson.class); + + // Phone or email has to be specified for the bean + Validator<BookPerson> phoneOrEmail = Validator.from( + personBean -> !"".equals(personBean.getPhone()) + || !"".equals(personBean.getEmail()), + "A person must have either a phone number or an email address"); + binder.withValidator(phoneOrEmail); + + binder.forField(emailField).bind("email"); + binder.forField(phoneField).bind("phone"); + + // Person person = // e.g. JPA entity or bean from Grid + BookPerson person = new BookPerson(1900, 5); + person.setEmail("Old Email"); + // Load person data to a form + binder.load(person); + + Button saveButton = new Button("Save", event -> { + // Using saveIfValid to avoid the try-catch block that is + // needed if using the regular save method + if (binder.saveIfValid(person)) { + // Person is valid and updated + // TODO Store in the database + } + }); + + emailField.setValue("foo@bar.com"); + Assert.assertTrue(binder.saveIfValid(person)); + // Person updated + Assert.assertEquals("foo@bar.com", person.getEmail()); + + emailField.setValue(""); + Assert.assertFalse(binder.saveIfValid(person)); + // Person updated because phone and email are both empty + Assert.assertEquals("foo@bar.com", person.getEmail()); + } + } diff --git a/server/src/test/java/com/vaadin/data/BinderTest.java b/server/src/test/java/com/vaadin/data/BinderTest.java index be408d6142..4c59773b2f 100644 --- a/server/src/test/java/com/vaadin/data/BinderTest.java +++ b/server/src/test/java/com/vaadin/data/BinderTest.java @@ -5,7 +5,9 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -217,10 +219,46 @@ public class BinderTest { try { binder.save(person); } finally { + // Bean should not have been updated Assert.assertEquals(firstName, person.getFirstName()); } } + @Test(expected = ValidationException.class) + public void save_beanValidationErrors() throws ValidationException { + Binder<Person> binder = new Binder<>(); + binder.forField(nameField).withValidator(new NotEmptyValidator<>("a")) + .bind(Person::getFirstName, Person::setFirstName); + + binder.withValidator(Validator.from(person -> false, "b")); + + Person person = new Person(); + nameField.setValue("foo"); + try { + binder.save(person); + } finally { + // Bean should have been updated for item validation but reverted + Assert.assertNull(person.getFirstName()); + } + } + + @Test + public void save_fieldsAndBeanLevelValidation() throws ValidationException { + Binder<Person> binder = new Binder<>(); + binder.forField(nameField).withValidator(new NotEmptyValidator<>("a")) + .bind(Person::getFirstName, Person::setFirstName); + + binder.withValidator( + Validator.from(person -> person.getLastName() != null, "b")); + + Person person = new Person(); + person.setLastName("bar"); + nameField.setValue("foo"); + binder.save(person); + Assert.assertEquals(nameField.getValue(), person.getFirstName()); + Assert.assertEquals("bar", person.getLastName()); + } + @Test public void saveIfValid_fieldValidationErrors() { Binder<Person> binder = new Binder<>(); @@ -251,6 +289,60 @@ public class BinderTest { } @Test + public void saveIfValid_beanValidationErrors() { + Binder<Person> binder = new Binder<>(); + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + + String msg = "foo"; + binder.withValidator(Validator.<Person> from( + prsn -> prsn.getAddress() != null || prsn.getEmail() != null, + msg)); + + Person person = new Person(); + person.setFirstName("foo"); + nameField.setValue(""); + Assert.assertFalse(binder.saveIfValid(person)); + + Assert.assertEquals("foo", person.getFirstName()); + } + + @Test + public void save_validationErrors_exceptionContainsErrors() + throws ValidationException { + Binder<Person> binder = new Binder<>(); + String msg = "foo"; + Binding<Person, String, String> nameBinding = binder.forField(nameField) + .withValidator(new NotEmptyValidator<>(msg)); + nameBinding.bind(Person::getFirstName, Person::setFirstName); + + Binding<Person, String, Integer> ageBinding = binder.forField(ageField) + .withConverter(stringToInteger).withValidator(notNegative); + ageBinding.bind(Person::getAge, Person::setAge); + + Person person = new Person(); + nameField.setValue(""); + ageField.setValue("-1"); + try { + binder.save(person); + Assert.fail(); + } catch (ValidationException exception) { + List<ValidationError<?>> validationErrors = exception + .getValidationError(); + Assert.assertEquals(2, validationErrors.size()); + ValidationError<?> error = validationErrors.get(0); + Assert.assertEquals(nameField, error.getField().get()); + Assert.assertEquals(msg, error.getMessage()); + Assert.assertEquals("", error.getValue()); + + error = validationErrors.get(1); + Assert.assertEquals(ageField, error.getField().get()); + Assert.assertEquals("Value must be positive", error.getMessage()); + Assert.assertEquals(ageField.getValue(), error.getValue()); + } + } + + @Test public void load_bound_fieldValueIsUpdated() { Binder<Person> binder = new Binder<>(); binder.bind(nameField, Person::getFirstName, Person::setFirstName); @@ -328,6 +420,7 @@ public class BinderTest { Assert.assertTrue(errorMessages.contains(msg1)); Set<?> fields = errors.stream().map(ValidationError::getField) + .filter(Optional::isPresent).map(Optional::get) .collect(Collectors.toSet()); Assert.assertEquals(1, fields.size()); Assert.assertTrue(fields.contains(nameField)); @@ -637,4 +730,139 @@ public class BinderTest { }); } + @Test + public void validate_failedBeanValidatorWithoutFieldValidators() { + Binder<Person> binder = new Binder<>(); + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + + String msg = "foo"; + binder.withValidator(Validator.from(bean -> false, msg)); + Person person = new Person(); + binder.bind(person); + + List<ValidationError<?>> errors = binder.validate(); + Assert.assertEquals(1, errors.size()); + Assert.assertFalse(errors.get(0).getField().isPresent()); + } + + @Test + public void validate_failedBeanValidatorWithFieldValidator() { + String msg = "foo"; + + Binder<Person> binder = new Binder<>(); + Binding<Person, String, String> binding = binder.forField(nameField) + .withValidator(new NotEmptyValidator<>(msg)); + binding.bind(Person::getFirstName, Person::setFirstName); + + binder.withValidator(Validator.from(bean -> false, msg)); + Person person = new Person(); + binder.bind(person); + + List<ValidationError<?>> errors = binder.validate(); + Assert.assertEquals(1, errors.size()); + ValidationError<?> error = errors.get(0); + Assert.assertEquals(msg, error.getMessage()); + Assert.assertTrue(error.getField().isPresent()); + Assert.assertEquals(nameField.getValue(), error.getValue()); + } + + @Test + public void validate_failedBothBeanValidatorAndFieldValidator() { + String msg1 = "foo"; + + Binder<Person> binder = new Binder<>(); + Binding<Person, String, String> binding = binder.forField(nameField) + .withValidator(new NotEmptyValidator<>(msg1)); + binding.bind(Person::getFirstName, Person::setFirstName); + + String msg2 = "bar"; + binder.withValidator(Validator.from(bean -> false, msg2)); + Person person = new Person(); + binder.bind(person); + + List<ValidationError<?>> errors = binder.validate(); + Assert.assertEquals(1, errors.size()); + + ValidationError<?> error = errors.get(0); + + Assert.assertEquals(msg1, error.getMessage()); + Assert.assertEquals(nameField, error.getField().get()); + Assert.assertEquals(nameField.getValue(), error.getValue()); + } + + @Test + public void validate_okBeanValidatorWithoutFieldValidators() { + Binder<Person> binder = new Binder<>(); + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + + String msg = "foo"; + binder.withValidator(Validator.from(bean -> true, msg)); + Person person = new Person(); + binder.bind(person); + + List<ValidationError<?>> errors = binder.validate(); + Assert.assertEquals(0, errors.size()); + } + + @Test + public void binder_saveIfValid() { + String msg1 = "foo"; + Binder<Person> binder = new Binder<>(); + Binding<Person, String, String> binding = binder.forField(nameField) + .withValidator(new NotEmptyValidator<>(msg1)); + binding.bind(Person::getFirstName, Person::setFirstName); + + String beanValidatorErrorMessage = "bar"; + binder.withValidator( + Validator.from(bean -> false, beanValidatorErrorMessage)); + Person person = new Person(); + String firstName = "first name"; + person.setFirstName(firstName); + binder.load(person); + + nameField.setValue(""); + Assert.assertFalse(binder.saveIfValid(person)); + // check that field level-validation failed and bean is not updated + Assert.assertEquals(firstName, person.getFirstName()); + + nameField.setValue("new name"); + + Assert.assertFalse(binder.saveIfValid(person)); + // Bean is updated but reverted + Assert.assertEquals(firstName, person.getFirstName()); + } + + @Test + public void updateBoundField_bindingValdationFails_beanLevelValidationIsNotRun() { + bindAgeWithValidatorConverterValidator(); + bindName(); + + AtomicBoolean beanLevelValidationRun = new AtomicBoolean(); + binder.withValidator(Validator.<Person> from( + bean -> beanLevelValidationRun.getAndSet(true), "")); + + ageField.setValue("not a number"); + + Assert.assertFalse(beanLevelValidationRun.get()); + + nameField.setValue("foo"); + Assert.assertFalse(beanLevelValidationRun.get()); + } + + @Test + public void updateBoundField_bindingValdationSuccess_beanLevelValidationIsRun() { + bindAgeWithValidatorConverterValidator(); + bindName(); + + AtomicBoolean beanLevelValidationRun = new AtomicBoolean(); + binder.withValidator(Validator.<Person> from( + bean -> beanLevelValidationRun.getAndSet(true), "")); + + ageField.setValue(String.valueOf(12)); + + Assert.assertTrue(beanLevelValidationRun.get()); + } + } |