diff options
author | Denis Anisimov <denis@vaadin.com> | 2016-10-03 15:42:10 +0300 |
---|---|---|
committer | Aleksi Hietanen <aleksi@vaadin.com> | 2016-10-06 08:44:50 +0000 |
commit | 3a69a723e689b047325ed88392d012ac96f4f62d (patch) | |
tree | 0e0034dcc7bd78e403aaad517fe242021ef1ca69 | |
parent | 6abb9c5c64c60db47e8f2dde87afaf0e76b2228c (diff) | |
download | vaadin-framework-3a69a723e689b047325ed88392d012ac96f4f62d.tar.gz vaadin-framework-3a69a723e689b047325ed88392d012ac96f4f62d.zip |
Add support for binder status change events (#208).
Change-Id: Ic8dee407569ee310f007ebe32660a1d2922e9493
5 files changed, 764 insertions, 24 deletions
diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index d2114e8120..7a59b90946 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -34,6 +34,7 @@ import java.util.stream.Collectors; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.StringToIntegerConverter; +import com.vaadin.event.EventRouter; import com.vaadin.server.ErrorMessage; import com.vaadin.server.UserError; import com.vaadin.shared.Registration; @@ -435,6 +436,7 @@ public class Binder<BEAN> implements Serializable { this.setter = setter; getBinder().bindings.add(this); getBinder().getBean().ifPresent(this::bind); + getBinder().fireStatusChangeEvent(false); } @Override @@ -533,6 +535,7 @@ public class Binder<BEAN> implements Serializable { getBinder().getValidationStatusHandler() .accept(new BinderValidationStatus<>(getBinder(), Arrays.asList(status), Collections.emptyList())); + getBinder().fireStatusChangeEvent(status.isError()); return status; } @@ -544,8 +547,8 @@ public class Binder<BEAN> implements Serializable { */ private ValidationStatus<TARGET> doValidation() { FIELDVALUE fieldValue = field.getValue(); - Result<TARGET> dataValue = converterValidatorChain.convertToModel( - fieldValue, findLocale()); + Result<TARGET> dataValue = converterValidatorChain + .convertToModel(fieldValue, findLocale()); return new ValidationStatus<>(this, dataValue); } @@ -566,8 +569,8 @@ public class Binder<BEAN> implements Serializable { } private FIELDVALUE convertDataToFieldType(BEAN bean) { - return converterValidatorChain.convertToPresentation( - getter.apply(bean), findLocale()); + return converterValidatorChain + .convertToPresentation(getter.apply(bean), findLocale()); } /** @@ -577,7 +580,7 @@ public class Binder<BEAN> implements Serializable { * the new value */ private void handleFieldValueChange(BEAN bean) { - binder.setHasChanges(true); + getBinder().setHasChanges(true); // store field value if valid ValidationStatus<TARGET> fieldValidationStatus = storeFieldValue( bean); @@ -585,14 +588,15 @@ public class Binder<BEAN> implements Serializable { // if all field level validations pass, run bean level validation if (!getBinder().bindings.stream().map(BindingImpl::doValidation) .anyMatch(ValidationStatus::isError)) { - binderValidationResults = binder.validateItem(bean); + binderValidationResults = getBinder().validateItem(bean); } else { binderValidationResults = Collections.emptyList(); } - binder.getValidationStatusHandler() - .accept(new BinderValidationStatus<>(binder, - Arrays.asList(fieldValidationStatus), - binderValidationResults)); + BinderValidationStatus<BEAN> status = new BinderValidationStatus<>( + binder, Arrays.asList(fieldValidationStatus), + binderValidationResults); + getBinder().getValidationStatusHandler().accept(status); + getBinder().fireStatusChangeEvent(status.hasErrors()); } /** @@ -668,6 +672,8 @@ public class Binder<BEAN> implements Serializable { private final List<Validator<? super BEAN>> validators = new ArrayList<>(); + private EventRouter eventRouter; + private Label statusLabel; private BinderValidationStatusHandler statusHandler; @@ -742,9 +748,9 @@ public class Binder<BEAN> implements Serializable { @Override public Registration addValueChangeListener( ValueChangeListener<? super SELECTVALUE> listener) { - return select.addSelectionListener(e -> listener.accept( - new ValueChange<>(select, getValue(), e - .isUserOriginated()))); + return select.addSelectionListener( + e -> listener.accept(new ValueChange<>(select, + getValue(), e.isUserOriginated()))); } }); } @@ -916,7 +922,7 @@ public class Binder<BEAN> implements Serializable { * <pre> * class Feature { * public enum Browser { CHROME, EDGE, FIREFOX, IE, OPERA, SAFARI } - + * public Set<Browser> getSupportedBrowsers() { ... } * public void setSupportedBrowsers(Set<Browser> title) { ... } * } @@ -964,13 +970,14 @@ public class Binder<BEAN> implements Serializable { */ public void bind(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); - unbind(); + doUnbind(false); this.bean = bean; bindings.forEach(b -> b.bind(bean)); // if there has been field value change listeners that trigger // validation, need to make sure the validation errors are cleared getValidationStatusHandler() .accept(BinderValidationStatus.createUnresolvedStatus(this)); + fireStatusChangeEvent(false); } /** @@ -978,13 +985,7 @@ public class Binder<BEAN> implements Serializable { * nothing. */ public void unbind() { - setHasChanges(false); - if (bean != null) { - bean = null; - bindings.forEach(BindingImpl::unbind); - } - getValidationStatusHandler() - .accept(BinderValidationStatus.createUnresolvedStatus(this)); + doUnbind(true); } /** @@ -1009,6 +1010,7 @@ public class Binder<BEAN> implements Serializable { getValidationStatusHandler() .accept(BinderValidationStatus.createUnresolvedStatus(this)); + fireStatusChangeEvent(false); } /** @@ -1073,6 +1075,7 @@ public class Binder<BEAN> implements Serializable { * @return a list of field validation errors if such occur, otherwise a list * of bean validation errors. */ + @SuppressWarnings({ "rawtypes", "unchecked" }) private BinderValidationStatus<BEAN> doSaveIfValid(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); // First run fields level validation @@ -1080,6 +1083,7 @@ public class Binder<BEAN> implements Serializable { // If no validation errors then update bean if (bindingStatuses.stream().filter(ValidationStatus::isError).findAny() .isPresent()) { + fireStatusChangeEvent(true); return new BinderValidationStatus<>(this, bindingStatuses, Collections.emptyList()); } @@ -1092,8 +1096,9 @@ public class Binder<BEAN> implements Serializable { bindings.forEach(binding -> binding.storeFieldValue(bean)); // Now run bean level validation against the updated bean List<Result<?>> binderResults = validateItem(bean); - if (binderResults.stream().filter(Result::isError).findAny() - .isPresent()) { + boolean hasErrors = binderResults.stream().filter(Result::isError) + .findAny().isPresent(); + if (hasErrors) { // Item validator failed, revert values bindings.forEach((BindingImpl binding) -> binding.setBeanValue(bean, oldValues.get(binding))); @@ -1101,6 +1106,7 @@ public class Binder<BEAN> implements Serializable { // Save successful, reset hasChanges to false setHasChanges(false); } + fireStatusChangeEvent(hasErrors); return new BinderValidationStatus<>(this, bindingStatuses, binderResults); } @@ -1150,6 +1156,7 @@ public class Binder<BEAN> implements Serializable { bindingStatuses, validateItem(bean)); } getValidationStatusHandler().accept(validationStatus); + fireStatusChangeEvent(validationStatus.hasErrors()); return validationStatus; } @@ -1274,6 +1281,43 @@ public class Binder<BEAN> implements Serializable { } /** + * Adds status change listener to the binder. + * <p> + * The {@link Binder} status is changed whenever any of the following + * happens: + * <ul> + * <li>if it's bound and any of its bound field or select has been changed + * <li>{@link #save(Object)} or {@link #saveIfValid(Object)} is called + * <li>{@link #load(Object)} is called + * <li>{@link #bind(Object)} is called + * <li>{@link #unbind(Object)} is called + * <li>{@link Binding#bind(Function, BiConsumer)} is called + * <li>{@link Binder#validate()} or {@link Binding#validate()} is called + * </ul> + * + * @see #load(Object) + * @see #save(Object) + * @see #saveIfValid(Object) + * @see #bind(Object) + * @see #unbind() + * @see #forField(HasValue) + * @see #forSelect(AbstractMultiSelect) + * @See {@link #validate()} + * @see Binding#validate() + * @see Binding#bind(Object) + * + * @param listener + * status change listener to add, not null + * @return a registration for the listener + */ + public Registration addStatusChangeListener(StatusChangeListener listener) { + getEventRouter().addListener(StatusChangeEvent.class, listener, + StatusChangeListener.class.getDeclaredMethods()[0]); + return () -> getEventRouter().removeListener(StatusChangeEvent.class, + listener); + } + + /** * Creates a new binding with the given field. * * @param <FIELDVALUE> @@ -1400,4 +1444,35 @@ public class Binder<BEAN> implements Serializable { public boolean hasChanges() { return hasChanges; } + + /** + * Returns the event router for this binder. + * + * @return the event router, not null + */ + protected EventRouter getEventRouter() { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + return eventRouter; + } + + private void doUnbind(boolean fireStatusEvent) { + setHasChanges(false); + if (bean != null) { + bean = null; + bindings.forEach(BindingImpl::unbind); + } + getValidationStatusHandler() + .accept(BinderValidationStatus.createUnresolvedStatus(this)); + if (fireStatusEvent) { + fireStatusChangeEvent(false); + } + } + + private void fireStatusChangeEvent(boolean hasValidationErrors) { + getEventRouter() + .fireEvent(new StatusChangeEvent(this, hasValidationErrors)); + } + } diff --git a/server/src/main/java/com/vaadin/data/StatusChangeEvent.java b/server/src/main/java/com/vaadin/data/StatusChangeEvent.java new file mode 100644 index 0000000000..7a73fb5150 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/StatusChangeEvent.java @@ -0,0 +1,86 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.util.EventObject; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import com.vaadin.data.Binder.Binding; + +/** + * Binder status change event. + * <p> + * The {@link Binder} status is changed whenever any of the following happens: + * <ul> + * <li>if it's bound and any of its bound field or select has been changed + * <li>{@link #save(Object)} or {@link #saveIfValid(Object)} is called + * <li>{@link #load(Object)} is called + * <li>{@link #bind(Object)} is called + * <li>{@link #unbind(Object)} is called + * <li>{@link Binding#bind(Function, BiConsumer)} is called + * <li>{@link Binder#validate()} or {@link Binding#validate()} is called + * </ul> + * + * @see StatusChangeListener#statusChange(StatusChangeEvent) + * @see Binder#addStatusChangeListener(StatusChangeListener) + * + * @author Vaadin Ltd + * + */ +public class StatusChangeEvent extends EventObject { + + private final boolean hasValidationErrors; + + /** + * Create a new status change event for given {@code binder} using its + * current validation status. + * + * @param binder + * the event source binder + * @param hasValidationErrors + * the binder validation status + */ + public StatusChangeEvent(Binder<?> binder, boolean hasValidationErrors) { + super(binder); + this.hasValidationErrors = hasValidationErrors; + } + + /** + * Gets the binder validation status. + * + * @return {@code true} if the binder has validation errors, {@code false} + * otherwise + */ + public boolean hasValidationErrors() { + return hasValidationErrors; + } + + @Override + public Binder<?> getSource() { + return (Binder<?>) super.getSource(); + } + + /** + * Gets the binder. + * + * @return the binder + */ + public Binder<?> getBinder() { + return getSource(); + } + +} diff --git a/server/src/main/java/com/vaadin/data/StatusChangeListener.java b/server/src/main/java/com/vaadin/data/StatusChangeListener.java new file mode 100644 index 0000000000..cb6afead24 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/StatusChangeListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.io.Serializable; + +/** + * Listener interface for {@link StatusChangeEvent}s. + * + * @see StatusChangeEvent + * @author Vaadin Ltd + * + */ +@FunctionalInterface +public interface StatusChangeListener extends Serializable { + + /** + * Notifies the listener about status change {@code event}. + * + * @param event + * a status change event, not null + */ + void statusChange(StatusChangeEvent event); +} diff --git a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java index 0b55bc8cad..f80f0781c2 100644 --- a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java +++ b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java @@ -18,11 +18,13 @@ package com.vaadin.data; import java.time.LocalDate; import java.util.List; import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import org.junit.Assert; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import com.vaadin.data.Binder.Binding; @@ -30,6 +32,7 @@ import com.vaadin.data.ValidationStatus.Status; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.StringToIntegerConverter; import com.vaadin.data.validator.EmailValidator; +import com.vaadin.data.validator.StringLengthValidator; import com.vaadin.server.AbstractErrorMessage; import com.vaadin.ui.Button; import com.vaadin.ui.DateField; @@ -712,4 +715,120 @@ public class BinderBookOfVaadinTest { formStatusLabel.getValue()); } + + @Test + @Ignore + public void statusChangeListener_binderIsNotBound() { + Button saveButton = new Button(); + Button resetButton = new Button(); + + AtomicBoolean eventIsFired = new AtomicBoolean(false); + + binder.addStatusChangeListener(event -> { + boolean isValid = !event.hasValidationErrors(); + boolean hasChanges = event.getBinder().hasChanges(); + eventIsFired.set(true); + + saveButton.setEnabled(hasChanges && isValid); + resetButton.setEnabled(hasChanges); + }); + binder.forField(field) + .withValidator(new StringLengthValidator("", 1, 3)) + .bind(BookPerson::getLastName, BookPerson::setLastName); + // no changes + Assert.assertFalse(saveButton.isEnabled()); + Assert.assertFalse(resetButton.isEnabled()); + verifyEventIsFired(eventIsFired); + + BookPerson person = new BookPerson(2000, 1); + binder.load(person); + // no changes + Assert.assertFalse(saveButton.isEnabled()); + Assert.assertFalse(resetButton.isEnabled()); + verifyEventIsFired(eventIsFired); + + field.setValue("a"); + // binder is not bound, no event fired + // no changes: see #375. There should be a change and enabled state + Assert.assertTrue(saveButton.isEnabled()); + Assert.assertTrue(resetButton.isEnabled()); + Assert.assertTrue(eventIsFired.get()); + + binder.saveIfValid(person); + // no changes + Assert.assertFalse(saveButton.isEnabled()); + Assert.assertFalse(resetButton.isEnabled()); + verifyEventIsFired(eventIsFired); + + binder.validate(); + // no changes + Assert.assertFalse(saveButton.isEnabled()); + Assert.assertFalse(resetButton.isEnabled()); + verifyEventIsFired(eventIsFired); + + field.setValue(""); + // binder is not bound, no event fired + // no changes: see #375. There should be a change and disabled state for + // save button because of failed validation + Assert.assertFalse(saveButton.isEnabled()); + Assert.assertTrue(resetButton.isEnabled()); + Assert.assertTrue(eventIsFired.get()); + } + + @Test + public void statusChangeListener_binderIsBound() { + Button saveButton = new Button(); + Button resetButton = new Button(); + + AtomicBoolean eventIsFired = new AtomicBoolean(false); + + binder.addStatusChangeListener(event -> { + boolean isValid = !event.hasValidationErrors(); + boolean hasChanges = event.getBinder().hasChanges(); + eventIsFired.set(true); + + saveButton.setEnabled(hasChanges && isValid); + resetButton.setEnabled(hasChanges); + }); + binder.forField(field) + .withValidator(new StringLengthValidator("", 1, 3)) + .bind(BookPerson::getLastName, BookPerson::setLastName); + // no changes + Assert.assertFalse(saveButton.isEnabled()); + Assert.assertFalse(resetButton.isEnabled()); + verifyEventIsFired(eventIsFired); + + BookPerson person = new BookPerson(2000, 1); + binder.bind(person); + // no changes + Assert.assertFalse(saveButton.isEnabled()); + Assert.assertFalse(resetButton.isEnabled()); + verifyEventIsFired(eventIsFired); + + field.setValue("a"); + // there are valid changes + Assert.assertTrue(saveButton.isEnabled()); + Assert.assertTrue(resetButton.isEnabled()); + verifyEventIsFired(eventIsFired); + + field.setValue(""); + // there are invalid changes + Assert.assertFalse(saveButton.isEnabled()); + Assert.assertTrue(resetButton.isEnabled()); + verifyEventIsFired(eventIsFired); + + // set valid value + field.setValue("a"); + verifyEventIsFired(eventIsFired); + binder.saveIfValid(person); + // there are no changes. + Assert.assertFalse(saveButton.isEnabled()); + Assert.assertFalse(resetButton.isEnabled()); + verifyEventIsFired(eventIsFired); + } + + private void verifyEventIsFired(AtomicBoolean flag) { + Assert.assertTrue(flag.get()); + flag.set(false); + } } diff --git a/server/src/test/java/com/vaadin/data/BinderStatusChangeTest.java b/server/src/test/java/com/vaadin/data/BinderStatusChangeTest.java new file mode 100644 index 0000000000..42550a0862 --- /dev/null +++ b/server/src/test/java/com/vaadin/data/BinderStatusChangeTest.java @@ -0,0 +1,423 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Binder.Binding; +import com.vaadin.data.util.converter.StringToIntegerConverter; +import com.vaadin.tests.data.bean.Person; + +/** + * @author Vaadin Ltd + * + */ +public class BinderStatusChangeTest + extends BinderTestBase<Binder<Person>, Person> { + + private AtomicReference<StatusChangeEvent> event; + + @Before + public void setUp() { + binder = new Binder<>(); + item = new Person(); + event = new AtomicReference<>(); + } + + @Test + public void bindBinding_unbound_eventWhenBoundEndnoEventsBeforeBound() { + binder.addStatusChangeListener(this::statusChanged); + + Binding<Person, String, String> binding = binder.forField(nameField); + + nameField.setValue(""); + Assert.assertNull(event.get()); + + binding.bind(Person::getFirstName, Person::setFirstName); + verifyEvent(); + } + + @Test + public void bindBinder_unbound_singleEventWhenBound() { + binder.addStatusChangeListener(this::statusChanged); + + Assert.assertNull(event.get()); + + binder.bind(item); + + verifyEvent(); + } + + @Test + public void unbindBinder_bound_singleEventWhenBound() { + binder.bind(item); + + unbindBinder_unbound_singleEventWhenBound(); + } + + @Test + public void unbindBinder_unbound_singleEventWhenBound() { + binder.addStatusChangeListener(this::statusChanged); + + Assert.assertNull(event.get()); + binder.unbind(); + verifyEvent(); + } + + @Test + public void setValue_bound_singleEventOnSetValue() { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.bind(item); + + binder.addStatusChangeListener(this::statusChanged); + + Assert.assertNull(event.get()); + nameField.setValue("foo"); + verifyEvent(); + } + + @Test + public void setValue_severalBoundFieldsAndBoundBinder_singleEventOnSetValue() { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.forField(ageField) + .withConverter(new StringToIntegerConverter("")) + .bind(Person::getAge, Person::setAge); + binder.bind(item); + + binder.addStatusChangeListener(this::statusChanged); + + Assert.assertNull(event.get()); + nameField.setValue("foo"); + verifyEvent(); + } + + @Test + public void setInvalidValue_bound_singleEventOnSetValue() { + binder.forField(nameField).withValidator(name -> false, "") + .bind(Person::getFirstName, Person::setFirstName); + binder.bind(item); + + binder.addStatusChangeListener(this::statusChanged); + + Assert.assertNull(event.get()); + nameField.setValue("foo"); + verifyEvent(true); + } + + @Test + public void setInvalidBean_bound_singleEventOnSetValue() { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.bind(item); + + binder.withValidator(Validator.from(bean -> false, "")); + + binder.addStatusChangeListener(this::statusChanged); + + Assert.assertNull(event.get()); + nameField.setValue("foo"); + verifyEvent(true); + } + + @Test + public void load_hasBindings_singleEventOnLoad() { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.load(item); + verifyEvent(); + } + + @Test + public void load_hasSeveralBindings_singleEventOnLoad() { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.forField(ageField) + .withConverter(new StringToIntegerConverter("")) + .bind(Person::getAge, Person::setAge); + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.load(item); + verifyEvent(); + } + + @Test + public void load_hasNoBindings_singleEvent() { + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.load(item); + verifyEvent(); + } + + @Test + public void save_hasNoBindings_singleEvent() throws ValidationException { + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.save(item); + verifyEvent(); + } + + @Test + public void saveIfValid_hasNoBindings_singleEvent() { + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.saveIfValid(item); + verifyEvent(); + } + + @Test + public void save_hasBindings_singleEvent() throws ValidationException { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.load(item); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.save(item); + verifyEvent(); + } + + @Test + public void save_hasSeveralBindings_singleEvent() + throws ValidationException { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.forField(ageField) + .withConverter(new StringToIntegerConverter("")) + .bind(Person::getAge, Person::setAge); + binder.load(item); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.save(item); + verifyEvent(); + } + + @Test + public void saveIfValid_hasBindings_singleEvent() { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.load(item); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.saveIfValid(item); + verifyEvent(); + } + + @Test + public void saveIfValid_hasSeveralBindings_singleEvent() { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.forField(ageField) + .withConverter(new StringToIntegerConverter("")) + .bind(Person::getAge, Person::setAge); + binder.load(item); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.saveIfValid(item); + verifyEvent(); + } + + @Test + public void saveInvalidValue_hasBindings_singleEvent() { + binder.forField(nameField).withValidator(name -> false, "") + .bind(Person::getFirstName, Person::setFirstName); + binder.load(item); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + try { + binder.save(item); + } catch (ValidationException ignore) { + } + verifyEvent(true); + } + + @Test + public void saveIfValid_invalidValueAndBinderHasBindings_singleEvent() { + binder.forField(nameField).withValidator(name -> false, "") + .bind(Person::getFirstName, Person::setFirstName); + binder.load(item); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.saveIfValid(item); + verifyEvent(true); + } + + @Test + public void saveIfValid_invalidValueAndBinderHasSeveralBindings_singleEvent() { + binder.forField(nameField).withValidator(name -> false, "") + .bind(Person::getFirstName, Person::setFirstName); + binder.forField(ageField) + .withConverter(new StringToIntegerConverter("")) + .bind(Person::getAge, Person::setAge); + binder.load(item); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.saveIfValid(item); + verifyEvent(true); + } + + @Test + public void saveInvalidBean_hasBindings_singleEvent() { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.load(item); + binder.withValidator(Validator.from(person -> false, "")); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + try { + binder.save(item); + } catch (ValidationException ignore) { + } + verifyEvent(true); + } + + @Test + public void saveIfValid_invalidBeanAndBinderHasBindings_singleEvent() { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.load(item); + binder.withValidator(Validator.from(person -> false, "")); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.saveIfValid(item); + verifyEvent(true); + } + + @Test + public void saveValidBean_hasBindings_singleEvent() + throws ValidationException { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.load(item); + binder.withValidator(Validator.from(person -> true, "")); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.save(item); + verifyEvent(); + } + + @Test + public void saveIfValid_validBeanAndBinderHasBindings_singleEvent() { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.load(item); + binder.withValidator(Validator.from(person -> true, "")); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + binder.saveIfValid(item); + verifyEvent(); + } + + @Test + public void validateBinder_noValidationErrors_statusEventWithoutErrors() { + binder.forField(nameField).bind(Person::getFirstName, + Person::setFirstName); + binder.forField(ageField) + .withConverter(new StringToIntegerConverter("")) + .bind(Person::getAge, Person::setAge); + binder.bind(item); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + + binder.validate(); + verifyEvent(); + } + + @Test + public void validateBinder_validationErrors_statusEventWithError() { + binder.forField(nameField).withValidator(name -> false, "") + .bind(Person::getFirstName, Person::setFirstName); + binder.forField(ageField) + .withConverter(new StringToIntegerConverter("")) + .bind(Person::getAge, Person::setAge); + binder.bind(item); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + + binder.validate(); + verifyEvent(true); + } + + @Test + public void validateBinding_noValidationErrors_statusEventWithoutErrors() { + Binding<Person, String, String> binding = binder.forField(nameField); + binding.bind(Person::getFirstName, Person::setFirstName); + binder.forField(ageField) + .withConverter(new StringToIntegerConverter("")) + .bind(Person::getAge, Person::setAge); + binder.bind(item); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + + binding.validate(); + verifyEvent(); + } + + @Test + public void validateBinding_validationErrors_statusEventWithError() { + Binding<Person, String, String> binding = binder.forField(nameField) + .withValidator(name -> false, ""); + binding.bind(Person::getFirstName, Person::setFirstName); + binder.forField(ageField) + .withConverter(new StringToIntegerConverter("")) + .bind(Person::getAge, Person::setAge); + binder.bind(item); + + binder.addStatusChangeListener(this::statusChanged); + Assert.assertNull(event.get()); + + binding.validate(); + verifyEvent(true); + } + + private void verifyEvent() { + verifyEvent(false); + } + + private void verifyEvent(boolean validationErrors) { + StatusChangeEvent statusChangeEvent = event.get(); + Assert.assertNotNull(statusChangeEvent); + Assert.assertEquals(binder, statusChangeEvent.getBinder()); + Assert.assertEquals(binder, statusChangeEvent.getSource()); + Assert.assertEquals(validationErrors, + statusChangeEvent.hasValidationErrors()); + } + + private void statusChanged(StatusChangeEvent evt) { + Assert.assertNull(event.get()); + event.set(evt); + } +} |