Skip to content

Commit debfc3b

Browse files
TatuLundAnsku
authored andcommitted
Cherry picks of Binder fixes in Flow (#11758)
* Cherry picks of Binder fixes in Flow Addresses: #9000 Addresses: #11109 These changes are adopted from vaadin/flow#4138 and vaadin/flow#6757
1 parent a8310a6 commit debfc3b

File tree

3 files changed

+184
-21
lines changed

3 files changed

+184
-21
lines changed

server/src/main/java/com/vaadin/data/Binder.java

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,7 @@ public Binding<BEAN, TARGET> bind(ValueProvider<BEAN, TARGET> getter,
816816

817817
getBinder().bindings.add(binding);
818818
if (getBinder().getBean() != null) {
819-
binding.initFieldValue(getBinder().getBean());
819+
binding.initFieldValue(getBinder().getBean(), true);
820820
}
821821
if (setter == null) {
822822
binding.getField().setReadOnly(true);
@@ -1146,20 +1146,33 @@ protected ValueContext createValueContext() {
11461146
*
11471147
* @param bean
11481148
* the bean to fetch the property value from
1149+
* @param writeBackChangedValues
1150+
* <code>true</code> if the bean value should be updated if
1151+
* the value is different after converting to and from the
1152+
* presentation value; <code>false</code> to avoid updating
1153+
* the bean value
11491154
*/
1150-
private void initFieldValue(BEAN bean) {
1155+
private void initFieldValue(BEAN bean, boolean writeBackChangedValues) {
11511156
assert bean != null;
11521157
assert onValueChange != null;
11531158
valueInit = true;
11541159
try {
1155-
getField().setValue(convertDataToFieldType(bean));
1160+
TARGET originalValue = getter.apply(bean);
1161+
convertAndSetFieldValue(originalValue);
1162+
1163+
if (writeBackChangedValues && setter != null) {
1164+
doConversion().ifOk(convertedValue -> {
1165+
if (!Objects.equals(originalValue, convertedValue)) {
1166+
setter.accept(bean, convertedValue);
1167+
}
1168+
});
1169+
}
11561170
} finally {
11571171
valueInit = false;
11581172
}
11591173
}
11601174

1161-
private FIELDVALUE convertDataToFieldType(BEAN bean) {
1162-
TARGET target = getter.apply(bean);
1175+
private FIELDVALUE convertToFieldType(TARGET target) {
11631176
ValueContext valueContext = createValueContext();
11641177
return converterValidatorChain.convertToPresentation(target,
11651178
valueContext);
@@ -1218,7 +1231,31 @@ public BindingValidationStatusHandler getValidationStatusHandler() {
12181231

12191232
@Override
12201233
public void read(BEAN bean) {
1221-
getField().setValue(convertDataToFieldType(bean));
1234+
convertAndSetFieldValue(getter.apply(bean));
1235+
}
1236+
1237+
private void convertAndSetFieldValue(TARGET modelValue) {
1238+
FIELDVALUE convertedValue = convertToFieldType(modelValue);
1239+
try {
1240+
getField().setValue(convertedValue);
1241+
} catch (RuntimeException e) {
1242+
/*
1243+
* Add an additional hint to the exception for the typical case
1244+
* with a field that doesn't accept null values. The non-null
1245+
* empty value is used as a heuristic to determine that the
1246+
* field doesn't accept null rather than throwing for some other
1247+
* reason.
1248+
*/
1249+
if (convertedValue == null && getField().getEmptyValue() != null) {
1250+
throw new IllegalStateException(String.format(
1251+
"A field of type %s didn't accept a null value."
1252+
+ " If null values are expected, then configure a null representation for the binding.",
1253+
field.getClass().getName()), e);
1254+
} else {
1255+
// Otherwise, let the original exception speak for itself
1256+
throw e;
1257+
}
1258+
}
12221259
}
12231260

12241261
@Override
@@ -1639,6 +1676,10 @@ public <FIELDVALUE> Binding<BEAN, FIELDVALUE> bind(
16391676
* Any change made in the fields also runs validation for the field
16401677
* {@link Binding} and bean level validation for this binder (bean level
16411678
* validators are added using {@link Binder#withValidator(Validator)}.
1679+
* <p>
1680+
* After updating each field, the value is read back from the field and the
1681+
* bean's property value is updated if it has been changed from the original
1682+
* value by the field or a converter.
16421683
*
16431684
* @see #readBean(Object)
16441685
* @see #writeBean(Object)
@@ -1658,7 +1699,7 @@ public void setBean(BEAN bean) {
16581699
} else {
16591700
doRemoveBean(false);
16601701
this.bean = bean;
1661-
getBindings().forEach(b -> b.initFieldValue(bean));
1702+
getBindings().forEach(b -> b.initFieldValue(bean, true));
16621703
// if there has been field value change listeners that trigger
16631704
// validation, need to make sure the validation errors are cleared
16641705
getValidationStatusHandler().statusChange(
@@ -1706,7 +1747,7 @@ public void readBean(BEAN bean) {
17061747
// we unbind a binding in valueChangeListener of another
17071748
// field.
17081749
if (binding.getField() != null)
1709-
binding.initFieldValue(bean);
1750+
binding.initFieldValue(bean, false);
17101751
});
17111752
getValidationStatusHandler().statusChange(
17121753
BinderValidationStatus.createUnresolvedStatus(this));

server/src/test/java/com/vaadin/data/BinderComponentTest.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,13 @@ public void checkboxgroup_bind_null() {
6666

6767
private <T> void testFieldNullRepresentation(T initialValue,
6868
HasValue<T> field) {
69-
binder.bind(field, t -> null, (str, val) -> {
70-
assertEquals("Value update with initial value failed.",
71-
initialValue, field.getValue());
72-
});
69+
binder.bind(field, t -> null, (str, val) -> {});
7370
field.setValue(initialValue);
7471
assertEquals("Initial value of field unexpected", initialValue,
7572
field.getValue());
7673
binder.setBean(item);
7774
assertEquals("Null representation for field failed",
7875
field.getEmptyValue(), field.getValue());
79-
field.setValue(initialValue);
8076
}
8177

8278
}

server/src/test/java/com/vaadin/data/BinderTest.java

Lines changed: 134 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@
88
import static org.junit.Assert.assertNull;
99
import static org.junit.Assert.assertSame;
1010
import static org.junit.Assert.assertTrue;
11+
import static org.junit.Assert.fail;
1112

1213
import java.util.Locale;
1314
import java.util.Objects;
1415
import java.util.concurrent.atomic.AtomicBoolean;
1516
import java.util.concurrent.atomic.AtomicInteger;
17+
import java.util.concurrent.atomic.AtomicReference;
1618
import java.util.stream.Stream;
1719

1820
import org.junit.Assert;
1921
import org.junit.Before;
2022
import org.junit.Test;
23+
import org.junit.Rule;
24+
import org.junit.rules.ExpectedException;
2125

2226
import com.vaadin.data.Binder.Binding;
2327
import com.vaadin.data.Binder.BindingBuilder;
@@ -31,9 +35,17 @@
3135
import com.vaadin.tests.data.bean.Sex;
3236
import com.vaadin.ui.TextField;
3337
import org.apache.commons.lang.StringUtils;
38+
import org.hamcrest.CoreMatchers;
3439

3540
public class BinderTest extends BinderTestBase<Binder<Person>, Person> {
3641

42+
@Rule
43+
/*
44+
* transient to avoid interfering with serialization tests that capture a
45+
* test instance in a closure
46+
*/
47+
public transient ExpectedException exceptionRule = ExpectedException.none();
48+
3749
@Before
3850
public void setUp() {
3951
binder = new Binder<>();
@@ -333,7 +345,7 @@ public String getEmptyValue() {
333345
binder.setBean(namelessPerson);
334346

335347
assertTrue(nullTextField.isEmpty());
336-
assertEquals(null, namelessPerson.getFirstName());
348+
assertEquals("null", namelessPerson.getFirstName());
337349

338350
// Change value, see that textfield is not empty and bean is updated.
339351
nullTextField.setValue("");
@@ -525,15 +537,15 @@ public void setRequired_withCustomValidator_fieldGetsRequiredIndicatorAndValidat
525537
binding.bind(Person::getFirstName, Person::setFirstName);
526538
binder.setBean(item);
527539
assertNull(textField.getErrorMessage());
528-
assertEquals(0, invokes.get());
540+
assertEquals(1, invokes.get());
529541

530542
textField.setValue(" ");
531543
ErrorMessage errorMessage = textField.getErrorMessage();
532544
assertNotNull(errorMessage);
533545
assertEquals("Input&#32;is&#32;required&#46;",
534546
errorMessage.getFormattedHtmlMessage());
535547
// validation is done for all changed bindings once.
536-
assertEquals(1, invokes.get());
548+
assertEquals(2, invokes.get());
537549

538550
textField.setValue("value");
539551
assertNull(textField.getErrorMessage());
@@ -582,15 +594,15 @@ public String convertToPresentation(String value,
582594

583595
binder.setBean(item);
584596
assertNull(textField.getErrorMessage());
585-
assertEquals(0, invokes.get());
597+
assertEquals(1, invokes.get());
586598

587599
textField.setValue(" ");
588600
ErrorMessage errorMessage = textField.getErrorMessage();
589601
assertNotNull(errorMessage);
590602
assertEquals("Input&#32;required&#46;",
591603
errorMessage.getFormattedHtmlMessage());
592604
// validation is done for all changed bindings once.
593-
assertEquals(1, invokes.get());
605+
assertEquals(2, invokes.get());
594606

595607
textField.setValue("value");
596608
assertNull(textField.getErrorMessage());
@@ -1099,12 +1111,12 @@ public void info_validator_not_considered_error() {
10991111

11001112
binder.setBean(item);
11011113
ageField.setValue("3");
1102-
Assert.assertEquals(infoMessage,
1114+
assertEquals(infoMessage,
11031115
ageField.getComponentError().getFormattedHtmlMessage());
1104-
Assert.assertEquals(ErrorLevel.INFO,
1116+
assertEquals(ErrorLevel.INFO,
11051117
ageField.getComponentError().getErrorLevel());
11061118

1107-
Assert.assertEquals(3, item.getAge());
1119+
assertEquals(3, item.getAge());
11081120
}
11091121

11101122
@Test
@@ -1246,4 +1258,118 @@ public void valueChangeListenerOrder() {
12461258

12471259
nameField.setValue("Foo");
12481260
}
1261+
1262+
@Test
1263+
public void nonSymetricValue_setBean_writtenToBean() {
1264+
binder.bind(nameField, Person::getLastName, Person::setLastName);
1265+
1266+
assertNull(item.getLastName());
1267+
1268+
binder.setBean(item);
1269+
1270+
assertEquals("", item.getLastName());
1271+
}
1272+
1273+
@Test
1274+
public void nonSymmetricValue_readBean_beanNotTouched() {
1275+
binder.bind(nameField, Person::getLastName, Person::setLastName);
1276+
binder.addValueChangeListener(
1277+
event -> fail("No value change event should be fired"));
1278+
1279+
assertNull(item.getLastName());
1280+
1281+
binder.readBean(item);
1282+
1283+
assertNull(item.getLastName());
1284+
}
1285+
1286+
@Test
1287+
public void symetricValue_setBean_beanNotUpdated() {
1288+
binder.bind(nameField, Person::getFirstName, Person::setFirstName);
1289+
1290+
binder.setBean(new Person() {
1291+
@Override
1292+
public String getFirstName() {
1293+
return "First";
1294+
}
1295+
1296+
@Override
1297+
public void setFirstName(String firstName) {
1298+
fail("Setter should not be called");
1299+
}
1300+
});
1301+
}
1302+
1303+
@Test
1304+
public void nullRejetingField_nullValue_wrappedExceptionMentionsNullRepresentation() {
1305+
TextField field = createNullAnd42RejectingFieldWithEmptyValue("");
1306+
1307+
Binder<AtomicReference<Integer>> binder = createIntegerConverterBinder(
1308+
field);
1309+
1310+
exceptionRule.expect(IllegalStateException.class);
1311+
exceptionRule.expectMessage("null representation");
1312+
exceptionRule.expectCause(CoreMatchers.isA(NullPointerException.class));
1313+
1314+
binder.readBean(new AtomicReference<>());
1315+
}
1316+
1317+
1318+
@Test
1319+
public void nullRejetingField_otherRejectedValue_originalExceptionIsThrown() {
1320+
TextField field = createNullAnd42RejectingFieldWithEmptyValue("");
1321+
1322+
Binder<AtomicReference<Integer>> binder = createIntegerConverterBinder(
1323+
field);
1324+
1325+
exceptionRule.expect(IllegalArgumentException.class);
1326+
exceptionRule.expectMessage("42");
1327+
1328+
binder.readBean(new AtomicReference<>(Integer.valueOf(42)));
1329+
}
1330+
1331+
@Test(expected = NullPointerException.class)
1332+
public void nullAcceptingField_nullValue_originalExceptionIsThrown() {
1333+
/*
1334+
* Edge case with a field that throws for null but has null as the empty
1335+
* value. This is most likely the case if the field doesn't explicitly
1336+
* reject null values but is instead somehow broken so that any value is
1337+
* rejected.
1338+
*/
1339+
TextField field = createNullAnd42RejectingFieldWithEmptyValue(null);
1340+
1341+
Binder<AtomicReference<Integer>> binder = createIntegerConverterBinder(
1342+
field);
1343+
1344+
binder.readBean(new AtomicReference<>(null));
1345+
}
1346+
1347+
private TextField createNullAnd42RejectingFieldWithEmptyValue(
1348+
String emptyValue) {
1349+
return new TextField() {
1350+
@Override
1351+
public void setValue(String value) {
1352+
if (value == null) {
1353+
throw new NullPointerException("Null value");
1354+
} else if ("42".equals(value)) {
1355+
throw new IllegalArgumentException("42 is not allowed");
1356+
}
1357+
super.setValue(value);
1358+
}
1359+
1360+
@Override
1361+
public String getEmptyValue() {
1362+
return emptyValue;
1363+
}
1364+
};
1365+
}
1366+
1367+
private Binder<AtomicReference<Integer>> createIntegerConverterBinder(
1368+
TextField field) {
1369+
Binder<AtomicReference<Integer>> binder = new Binder<>();
1370+
binder.forField(field)
1371+
.withConverter(new StringToIntegerConverter("Must have number"))
1372+
.bind(AtomicReference::get, AtomicReference::set);
1373+
return binder;
1374+
}
12491375
}

0 commit comments

Comments
 (0)