summaryrefslogtreecommitdiffstats
path: root/server/src
diff options
context:
space:
mode:
authorDenis Anisimov <denis@vaadin.com>2016-08-25 14:53:09 +0300
committerArtur Signell <artur@vaadin.com>2016-09-02 15:13:19 +0300
commitccaabe6db025f7e73adc83b4d0e2671c7fa16d40 (patch)
treed1508def2ee24cac623616c1b4fdd29a6b8f069c /server/src
parent876b6383e6ec50a8bbe34126b7bfed5f6f616bea (diff)
downloadvaadin-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')
-rw-r--r--server/src/main/java/com/vaadin/data/BeanBinder.java6
-rw-r--r--server/src/main/java/com/vaadin/data/Binder.java234
-rw-r--r--server/src/main/java/com/vaadin/data/ValidationError.java66
-rw-r--r--server/src/test/java/com/vaadin/data/BeanBinderTest.java10
-rw-r--r--server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java97
-rw-r--r--server/src/test/java/com/vaadin/data/BinderTest.java228
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());
+ }
+
}