});
----
-[classname]#BeanBinder# will automatically run bean-level validation based on the used bean instance if it has been bound using the [methodname]#bind# method.
+We can add custom form validators to [classname]#Binder#. These will be run on the updated item instance (bean) after field validators have succeeded and the item has been updated. If item level validators fail, the values updated in the item instance will be reverted, i.e. the bean will temporarily contain new values but after a call to [methodname]#save# or [methodname]#saveIfValid#, the bean will only contain the new values if all validators passed.
-If we are using the [methodname]#load# and [methodname]#save# methods, then the binder will not have any bean instance to use for bean-level validation.
-We must use a copy of the bean for running bean-level validation if we want to make sure no changes are done to the original bean before we know that validation passes.
+[classname]#BeanBinder# will automatically add bean-level validation based on the used bean instance and its annotations.
[source, java]
----
-Button saveButton = new Button("Save", event -> {
- // Create non-shared copy to use for validation
- Person copy = new Person(person);
+BeanBinder<Person> binder = new BeanBinder<Person>(
+ Person.class);
- List<ValidationError<?>> errors = binder.validateWithBean(copy);
- if (errors.isEmpty()) {
- // Write new values to the actual bean
+// Phone or email has to be specified for the bean
+Validator<Person> 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);
- // Using saveIfValid to avoid the try-catch block that is
- // needed if using the regular save method
- binder.saveIfValid(person);
+binder.forField(emailField).bind("email");
+binder.forField(phoneField).bind("phone");
- // TODO: Do something with the updated person instance
+Person person = // e.g. JPA entity or bean from Grid
+// 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
}
-})
+});
----
+If we want to ensure that the [classname]#Person# instance is not even temporarily updated, we should make a clone and use that with [methodname]#saveIfValid#.
+
== Using Binder with Vaadin Designer
We can use [classname]#Binder# to connect data to a form that is designed using Vaadin Designer.
forField(field).bind(propertyName);
}
+ @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,
Objects.requireNonNull(converter, "converter cannot be null");
return new BeanBindingImpl<>(this, field, converter, handler);
}
+
}
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;
* 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.
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;
}
* 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() {
/**
* 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(),
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
* 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
bindings.forEach(b -> b.bind(bean));
}
- /**
- * 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.
/**
* 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
/**
* 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)
/**
* 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)
*
/**
* 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());
}
/**
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();
+ }
}
/**
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;
+ }
+
}
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() {
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());
}
}
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.
*/
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;
this.lastName = lastName;
}
- public BookPerson(int yearOfBirth, int salaryLevel) {
- this.yearOfBirth = yearOfBirth;
- this.salaryLevel = salaryLevel;
- }
-
public int getYearOfBirth() {
return yearOfBirth;
}
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 {
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
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());
+ }
+
}
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;
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<>();
Assert.assertEquals("bar", person.getFirstName());
}
+ @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<>();
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));
});
}
+ @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());
+ }
+
}