diff options
author | Tatu Lund <tatu@vaadin.com> | 2019-11-13 17:41:52 +0200 |
---|---|---|
committer | Anna Koskinen <Ansku@users.noreply.github.com> | 2019-11-13 17:41:52 +0200 |
commit | debfc3b0385e931a4cb4d694c61223674c2413c2 (patch) | |
tree | d8490d8b1160031d5e163c5a02214bf52a4e1d47 | |
parent | a8310a63c6f217e2d7af304dc34cd52a01261590 (diff) | |
download | vaadin-framework-debfc3b0385e931a4cb4d694c61223674c2413c2.tar.gz vaadin-framework-debfc3b0385e931a4cb4d694c61223674c2413c2.zip |
Cherry picks of Binder fixes in Flow (#11758)
* Cherry picks of Binder fixes in Flow
Addresses: https://github.com/vaadin/framework/issues/9000
Addresses: https://github.com/vaadin/framework/issues/11109
These changes are adopted from https://github.com/vaadin/flow/pull/4138 and https://github.com/vaadin/flow/pull/6757
-rw-r--r-- | server/src/main/java/com/vaadin/data/Binder.java | 57 | ||||
-rw-r--r-- | server/src/test/java/com/vaadin/data/BinderComponentTest.java | 6 | ||||
-rw-r--r-- | server/src/test/java/com/vaadin/data/BinderTest.java | 142 |
3 files changed, 184 insertions, 21 deletions
diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index 22ef7d7782..699185dea3 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -816,7 +816,7 @@ public class Binder<BEAN> implements Serializable { getBinder().bindings.add(binding); if (getBinder().getBean() != null) { - binding.initFieldValue(getBinder().getBean()); + binding.initFieldValue(getBinder().getBean(), true); } if (setter == null) { binding.getField().setReadOnly(true); @@ -1146,20 +1146,33 @@ public class Binder<BEAN> implements Serializable { * * @param bean * the bean to fetch the property value from + * @param writeBackChangedValues + * <code>true</code> if the bean value should be updated if + * the value is different after converting to and from the + * presentation value; <code>false</code> to avoid updating + * the bean value */ - private void initFieldValue(BEAN bean) { + private void initFieldValue(BEAN bean, boolean writeBackChangedValues) { assert bean != null; assert onValueChange != null; valueInit = true; try { - getField().setValue(convertDataToFieldType(bean)); + TARGET originalValue = getter.apply(bean); + convertAndSetFieldValue(originalValue); + + if (writeBackChangedValues && setter != null) { + doConversion().ifOk(convertedValue -> { + if (!Objects.equals(originalValue, convertedValue)) { + setter.accept(bean, convertedValue); + } + }); + } } finally { valueInit = false; } } - private FIELDVALUE convertDataToFieldType(BEAN bean) { - TARGET target = getter.apply(bean); + private FIELDVALUE convertToFieldType(TARGET target) { ValueContext valueContext = createValueContext(); return converterValidatorChain.convertToPresentation(target, valueContext); @@ -1218,7 +1231,31 @@ public class Binder<BEAN> implements Serializable { @Override public void read(BEAN bean) { - getField().setValue(convertDataToFieldType(bean)); + convertAndSetFieldValue(getter.apply(bean)); + } + + private void convertAndSetFieldValue(TARGET modelValue) { + FIELDVALUE convertedValue = convertToFieldType(modelValue); + try { + getField().setValue(convertedValue); + } catch (RuntimeException e) { + /* + * Add an additional hint to the exception for the typical case + * with a field that doesn't accept null values. The non-null + * empty value is used as a heuristic to determine that the + * field doesn't accept null rather than throwing for some other + * reason. + */ + if (convertedValue == null && getField().getEmptyValue() != null) { + throw new IllegalStateException(String.format( + "A field of type %s didn't accept a null value." + + " If null values are expected, then configure a null representation for the binding.", + field.getClass().getName()), e); + } else { + // Otherwise, let the original exception speak for itself + throw e; + } + } } @Override @@ -1639,6 +1676,10 @@ public class Binder<BEAN> implements Serializable { * 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)}. + * <p> + * After updating each field, the value is read back from the field and the + * bean's property value is updated if it has been changed from the original + * value by the field or a converter. * * @see #readBean(Object) * @see #writeBean(Object) @@ -1658,7 +1699,7 @@ public class Binder<BEAN> implements Serializable { } else { doRemoveBean(false); this.bean = bean; - getBindings().forEach(b -> b.initFieldValue(bean)); + getBindings().forEach(b -> b.initFieldValue(bean, true)); // if there has been field value change listeners that trigger // validation, need to make sure the validation errors are cleared getValidationStatusHandler().statusChange( @@ -1706,7 +1747,7 @@ public class Binder<BEAN> implements Serializable { // we unbind a binding in valueChangeListener of another // field. if (binding.getField() != null) - binding.initFieldValue(bean); + binding.initFieldValue(bean, false); }); getValidationStatusHandler().statusChange( BinderValidationStatus.createUnresolvedStatus(this)); diff --git a/server/src/test/java/com/vaadin/data/BinderComponentTest.java b/server/src/test/java/com/vaadin/data/BinderComponentTest.java index 776816be5e..ab5122cc76 100644 --- a/server/src/test/java/com/vaadin/data/BinderComponentTest.java +++ b/server/src/test/java/com/vaadin/data/BinderComponentTest.java @@ -66,17 +66,13 @@ public class BinderComponentTest private <T> void testFieldNullRepresentation(T initialValue, HasValue<T> field) { - binder.bind(field, t -> null, (str, val) -> { - assertEquals("Value update with initial value failed.", - initialValue, field.getValue()); - }); + binder.bind(field, t -> null, (str, val) -> {}); field.setValue(initialValue); assertEquals("Initial value of field unexpected", initialValue, field.getValue()); binder.setBean(item); assertEquals("Null representation for field failed", field.getEmptyValue(), field.getValue()); - field.setValue(initialValue); } } diff --git a/server/src/test/java/com/vaadin/data/BinderTest.java b/server/src/test/java/com/vaadin/data/BinderTest.java index f4515dc211..3e27cc5f9b 100644 --- a/server/src/test/java/com/vaadin/data/BinderTest.java +++ b/server/src/test/java/com/vaadin/data/BinderTest.java @@ -8,16 +8,20 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.util.Locale; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.ExpectedException; import com.vaadin.data.Binder.Binding; import com.vaadin.data.Binder.BindingBuilder; @@ -31,9 +35,17 @@ import com.vaadin.tests.data.bean.Person; import com.vaadin.tests.data.bean.Sex; import com.vaadin.ui.TextField; import org.apache.commons.lang.StringUtils; +import org.hamcrest.CoreMatchers; public class BinderTest extends BinderTestBase<Binder<Person>, Person> { + @Rule + /* + * transient to avoid interfering with serialization tests that capture a + * test instance in a closure + */ + public transient ExpectedException exceptionRule = ExpectedException.none(); + @Before public void setUp() { binder = new Binder<>(); @@ -333,7 +345,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { binder.setBean(namelessPerson); assertTrue(nullTextField.isEmpty()); - assertEquals(null, namelessPerson.getFirstName()); + assertEquals("null", namelessPerson.getFirstName()); // Change value, see that textfield is not empty and bean is updated. nullTextField.setValue(""); @@ -525,7 +537,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { binding.bind(Person::getFirstName, Person::setFirstName); binder.setBean(item); assertNull(textField.getErrorMessage()); - assertEquals(0, invokes.get()); + assertEquals(1, invokes.get()); textField.setValue(" "); ErrorMessage errorMessage = textField.getErrorMessage(); @@ -533,7 +545,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { assertEquals("Input is required.", errorMessage.getFormattedHtmlMessage()); // validation is done for all changed bindings once. - assertEquals(1, invokes.get()); + assertEquals(2, invokes.get()); textField.setValue("value"); assertNull(textField.getErrorMessage()); @@ -582,7 +594,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { binder.setBean(item); assertNull(textField.getErrorMessage()); - assertEquals(0, invokes.get()); + assertEquals(1, invokes.get()); textField.setValue(" "); ErrorMessage errorMessage = textField.getErrorMessage(); @@ -590,7 +602,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { assertEquals("Input required.", errorMessage.getFormattedHtmlMessage()); // validation is done for all changed bindings once. - assertEquals(1, invokes.get()); + assertEquals(2, invokes.get()); textField.setValue("value"); assertNull(textField.getErrorMessage()); @@ -1099,12 +1111,12 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { binder.setBean(item); ageField.setValue("3"); - Assert.assertEquals(infoMessage, + assertEquals(infoMessage, ageField.getComponentError().getFormattedHtmlMessage()); - Assert.assertEquals(ErrorLevel.INFO, + assertEquals(ErrorLevel.INFO, ageField.getComponentError().getErrorLevel()); - Assert.assertEquals(3, item.getAge()); + assertEquals(3, item.getAge()); } @Test @@ -1246,4 +1258,118 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { nameField.setValue("Foo"); } + + @Test + public void nonSymetricValue_setBean_writtenToBean() { + binder.bind(nameField, Person::getLastName, Person::setLastName); + + assertNull(item.getLastName()); + + binder.setBean(item); + + assertEquals("", item.getLastName()); + } + + @Test + public void nonSymmetricValue_readBean_beanNotTouched() { + binder.bind(nameField, Person::getLastName, Person::setLastName); + binder.addValueChangeListener( + event -> fail("No value change event should be fired")); + + assertNull(item.getLastName()); + + binder.readBean(item); + + assertNull(item.getLastName()); + } + + @Test + public void symetricValue_setBean_beanNotUpdated() { + binder.bind(nameField, Person::getFirstName, Person::setFirstName); + + binder.setBean(new Person() { + @Override + public String getFirstName() { + return "First"; + } + + @Override + public void setFirstName(String firstName) { + fail("Setter should not be called"); + } + }); + } + + @Test + public void nullRejetingField_nullValue_wrappedExceptionMentionsNullRepresentation() { + TextField field = createNullAnd42RejectingFieldWithEmptyValue(""); + + Binder<AtomicReference<Integer>> binder = createIntegerConverterBinder( + field); + + exceptionRule.expect(IllegalStateException.class); + exceptionRule.expectMessage("null representation"); + exceptionRule.expectCause(CoreMatchers.isA(NullPointerException.class)); + + binder.readBean(new AtomicReference<>()); + } + + + @Test + public void nullRejetingField_otherRejectedValue_originalExceptionIsThrown() { + TextField field = createNullAnd42RejectingFieldWithEmptyValue(""); + + Binder<AtomicReference<Integer>> binder = createIntegerConverterBinder( + field); + + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("42"); + + binder.readBean(new AtomicReference<>(Integer.valueOf(42))); + } + + @Test(expected = NullPointerException.class) + public void nullAcceptingField_nullValue_originalExceptionIsThrown() { + /* + * Edge case with a field that throws for null but has null as the empty + * value. This is most likely the case if the field doesn't explicitly + * reject null values but is instead somehow broken so that any value is + * rejected. + */ + TextField field = createNullAnd42RejectingFieldWithEmptyValue(null); + + Binder<AtomicReference<Integer>> binder = createIntegerConverterBinder( + field); + + binder.readBean(new AtomicReference<>(null)); + } + + private TextField createNullAnd42RejectingFieldWithEmptyValue( + String emptyValue) { + return new TextField() { + @Override + public void setValue(String value) { + if (value == null) { + throw new NullPointerException("Null value"); + } else if ("42".equals(value)) { + throw new IllegalArgumentException("42 is not allowed"); + } + super.setValue(value); + } + + @Override + public String getEmptyValue() { + return emptyValue; + } + }; + } + + private Binder<AtomicReference<Integer>> createIntegerConverterBinder( + TextField field) { + Binder<AtomicReference<Integer>> binder = new Binder<>(); + binder.forField(field) + .withConverter(new StringToIntegerConverter("Must have number")) + .bind(AtomicReference::get, AtomicReference::set); + return binder; + } } |