Browse Source

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
tags/8.10.0.alpha1
Tatu Lund 4 years ago
parent
commit
debfc3b038

+ 49
- 8
server/src/main/java/com/vaadin/data/Binder.java View File



getBinder().bindings.add(binding); getBinder().bindings.add(binding);
if (getBinder().getBean() != null) { if (getBinder().getBean() != null) {
binding.initFieldValue(getBinder().getBean());
binding.initFieldValue(getBinder().getBean(), true);
} }
if (setter == null) { if (setter == null) {
binding.getField().setReadOnly(true); binding.getField().setReadOnly(true);
* *
* @param bean * @param bean
* the bean to fetch the property value from * 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 bean != null;
assert onValueChange != null; assert onValueChange != null;
valueInit = true; valueInit = true;
try { 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 { } finally {
valueInit = false; valueInit = false;
} }
} }


private FIELDVALUE convertDataToFieldType(BEAN bean) {
TARGET target = getter.apply(bean);
private FIELDVALUE convertToFieldType(TARGET target) {
ValueContext valueContext = createValueContext(); ValueContext valueContext = createValueContext();
return converterValidatorChain.convertToPresentation(target, return converterValidatorChain.convertToPresentation(target,
valueContext); valueContext);


@Override @Override
public void read(BEAN bean) { 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 @Override
* Any change made in the fields also runs validation for the field * Any change made in the fields also runs validation for the field
* {@link Binding} and bean level validation for this binder (bean level * {@link Binding} and bean level validation for this binder (bean level
* validators are added using {@link Binder#withValidator(Validator)}. * 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 #readBean(Object)
* @see #writeBean(Object) * @see #writeBean(Object)
} else { } else {
doRemoveBean(false); doRemoveBean(false);
this.bean = bean; 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 // if there has been field value change listeners that trigger
// validation, need to make sure the validation errors are cleared // validation, need to make sure the validation errors are cleared
getValidationStatusHandler().statusChange( getValidationStatusHandler().statusChange(
// we unbind a binding in valueChangeListener of another // we unbind a binding in valueChangeListener of another
// field. // field.
if (binding.getField() != null) if (binding.getField() != null)
binding.initFieldValue(bean);
binding.initFieldValue(bean, false);
}); });
getValidationStatusHandler().statusChange( getValidationStatusHandler().statusChange(
BinderValidationStatus.createUnresolvedStatus(this)); BinderValidationStatus.createUnresolvedStatus(this));

+ 1
- 5
server/src/test/java/com/vaadin/data/BinderComponentTest.java View File



private <T> void testFieldNullRepresentation(T initialValue, private <T> void testFieldNullRepresentation(T initialValue,
HasValue<T> field) { 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); field.setValue(initialValue);
assertEquals("Initial value of field unexpected", initialValue, assertEquals("Initial value of field unexpected", initialValue,
field.getValue()); field.getValue());
binder.setBean(item); binder.setBean(item);
assertEquals("Null representation for field failed", assertEquals("Null representation for field failed",
field.getEmptyValue(), field.getValue()); field.getEmptyValue(), field.getValue());
field.setValue(initialValue);
} }


} }

+ 134
- 8
server/src/test/java/com/vaadin/data/BinderTest.java View File

import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;


import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream; import java.util.stream.Stream;


import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.ExpectedException;


import com.vaadin.data.Binder.Binding; import com.vaadin.data.Binder.Binding;
import com.vaadin.data.Binder.BindingBuilder; import com.vaadin.data.Binder.BindingBuilder;
import com.vaadin.tests.data.bean.Sex; import com.vaadin.tests.data.bean.Sex;
import com.vaadin.ui.TextField; import com.vaadin.ui.TextField;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.hamcrest.CoreMatchers;


public class BinderTest extends BinderTestBase<Binder<Person>, Person> { 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 @Before
public void setUp() { public void setUp() {
binder = new Binder<>(); binder = new Binder<>();
binder.setBean(namelessPerson); binder.setBean(namelessPerson);


assertTrue(nullTextField.isEmpty()); assertTrue(nullTextField.isEmpty());
assertEquals(null, namelessPerson.getFirstName());
assertEquals("null", namelessPerson.getFirstName());


// Change value, see that textfield is not empty and bean is updated. // Change value, see that textfield is not empty and bean is updated.
nullTextField.setValue(""); nullTextField.setValue("");
binding.bind(Person::getFirstName, Person::setFirstName); binding.bind(Person::getFirstName, Person::setFirstName);
binder.setBean(item); binder.setBean(item);
assertNull(textField.getErrorMessage()); assertNull(textField.getErrorMessage());
assertEquals(0, invokes.get());
assertEquals(1, invokes.get());


textField.setValue(" "); textField.setValue(" ");
ErrorMessage errorMessage = textField.getErrorMessage(); ErrorMessage errorMessage = textField.getErrorMessage();
assertEquals("Input&#32;is&#32;required&#46;", assertEquals("Input&#32;is&#32;required&#46;",
errorMessage.getFormattedHtmlMessage()); errorMessage.getFormattedHtmlMessage());
// validation is done for all changed bindings once. // validation is done for all changed bindings once.
assertEquals(1, invokes.get());
assertEquals(2, invokes.get());


textField.setValue("value"); textField.setValue("value");
assertNull(textField.getErrorMessage()); assertNull(textField.getErrorMessage());


binder.setBean(item); binder.setBean(item);
assertNull(textField.getErrorMessage()); assertNull(textField.getErrorMessage());
assertEquals(0, invokes.get());
assertEquals(1, invokes.get());


textField.setValue(" "); textField.setValue(" ");
ErrorMessage errorMessage = textField.getErrorMessage(); ErrorMessage errorMessage = textField.getErrorMessage();
assertEquals("Input&#32;required&#46;", assertEquals("Input&#32;required&#46;",
errorMessage.getFormattedHtmlMessage()); errorMessage.getFormattedHtmlMessage());
// validation is done for all changed bindings once. // validation is done for all changed bindings once.
assertEquals(1, invokes.get());
assertEquals(2, invokes.get());


textField.setValue("value"); textField.setValue("value");
assertNull(textField.getErrorMessage()); assertNull(textField.getErrorMessage());


binder.setBean(item); binder.setBean(item);
ageField.setValue("3"); ageField.setValue("3");
Assert.assertEquals(infoMessage,
assertEquals(infoMessage,
ageField.getComponentError().getFormattedHtmlMessage()); ageField.getComponentError().getFormattedHtmlMessage());
Assert.assertEquals(ErrorLevel.INFO,
assertEquals(ErrorLevel.INFO,
ageField.getComponentError().getErrorLevel()); ageField.getComponentError().getErrorLevel());


Assert.assertEquals(3, item.getAge());
assertEquals(3, item.getAge());
} }


@Test @Test


nameField.setValue("Foo"); 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;
}
} }

Loading…
Cancel
Save