Browse Source

Fix Binder bean writing to only validate and write given bindings (#10162)

tags/8.2.0.alpha3
Teemu Suo-Anttila 6 years ago
parent
commit
dd806e8bb3

+ 53
- 27
server/src/main/java/com/vaadin/data/Binder.java View File



/** /**
* Validates the field value and returns a {@code ValidationStatus} * Validates the field value and returns a {@code ValidationStatus}
* instance representing the outcome of the validation.
* instance representing the outcome of the validation. This method is a
* short-hand for calling {@link #validate(boolean)} with
* {@code fireEvent} {@code true}.
* *
* @see #validate(boolean)
* @see Binder#validate() * @see Binder#validate()
* @see Validator#apply(Object, ValueContext) * @see Validator#apply(Object, ValueContext)
* *
* @return the validation result. * @return the validation result.
*/ */
public BindingValidationStatus<TARGET> validate();
public default BindingValidationStatus<TARGET> validate() {
return validate(true);
}

/**
* Validates the field value and returns a {@code ValidationStatus}
* instance representing the outcome of the validation.
*
* @see #validate()
*
* @param fireEvent
* {@code true} to fire status event; {@code false} to not
* @return the validation result.
*
* @since 8.2
*/
public BindingValidationStatus<TARGET> validate(boolean fireEvent);


/** /**
* Gets the validation status handler for this Binding. * Gets the validation status handler for this Binding.
TARGET nullRepresentation) { TARGET nullRepresentation) {
return withConverter( return withConverter(
fieldValue -> Objects.equals(fieldValue, nullRepresentation) fieldValue -> Objects.equals(fieldValue, nullRepresentation)
? null
: fieldValue,
? null : fieldValue,
modelValue -> Objects.isNull(modelValue) modelValue -> Objects.isNull(modelValue)
? nullRepresentation
: modelValue);
? nullRepresentation : modelValue);
} }


/** /**
} }


@Override @Override
public BindingValidationStatus<TARGET> validate() {
public BindingValidationStatus<TARGET> validate(boolean fireEvent) {
Objects.requireNonNull(binder, Objects.requireNonNull(binder,
"This Binding is no longer attached to a Binder"); "This Binding is no longer attached to a Binder");
BindingValidationStatus<TARGET> status = doValidation(); BindingValidationStatus<TARGET> status = doValidation();
getBinder().getValidationStatusHandler()
.statusChange(new BinderValidationStatus<>(getBinder(),
Arrays.asList(status), Collections.emptyList()));
getBinder().fireStatusChangeEvent(status.isError());
if (fireEvent) {
getBinder().getValidationStatusHandler()
.statusChange(new BinderValidationStatus<>(getBinder(),
Arrays.asList(status),
Collections.emptyList()));
getBinder().fireStatusChangeEvent(status.isError());
}
return status; return status;
} }


* if some of the bound field values fail to validate * if some of the bound field values fail to validate
*/ */
public void writeBean(BEAN bean) throws ValidationException { public void writeBean(BEAN bean) throws ValidationException {
BinderValidationStatus<BEAN> status = doWriteIfValid(bean, bindings);
BinderValidationStatus<BEAN> status = doWriteIfValid(bean,
new ArrayList<>(bindings));
if (status.hasErrors()) { if (status.hasErrors()) {
throw new ValidationException(status.getFieldValidationErrors(), throw new ValidationException(status.getFieldValidationErrors(),
status.getBeanValidationErrors()); status.getBeanValidationErrors());
* updated, {@code false} otherwise * updated, {@code false} otherwise
*/ */
public boolean writeBeanIfValid(BEAN bean) { public boolean writeBeanIfValid(BEAN bean) {
return doWriteIfValid(bean, bindings).isOk();
return doWriteIfValid(bean, new ArrayList<>(bindings)).isOk();
} }


/** /**
* Writes the field values into the given bean if all field level validators * Writes the field values into the given bean if all field level validators
* pass. Runs bean level validators on the bean after writing. * pass. Runs bean level validators on the bean after writing.
* <p>
* <strong>Note:</strong> The collection of bindings is cleared on
* successful save.
* *
* @param bean * @param bean
* the bean to write field values into * the bean to write field values into
Objects.requireNonNull(bean, "bean cannot be null"); Objects.requireNonNull(bean, "bean cannot be null");
List<ValidationResult> binderResults = Collections.emptyList(); List<ValidationResult> binderResults = Collections.emptyList();


// First run fields level validation
List<BindingValidationStatus<?>> bindingStatuses = validateBindings();
// First run fields level validation, if no validation errors then
// update bean
List<BindingValidationStatus<?>> bindingResults = bindings.stream()
.map(b -> b.validate(false)).collect(Collectors.toList());


// If no validation errors then update bean
// TODO: Enable partial write when some fields don't pass
if (bindingStatuses.stream()
if (bindingResults.stream()
.noneMatch(BindingValidationStatus::isError)) { .noneMatch(BindingValidationStatus::isError)) {
// Store old bean values so we can restore them if validators fail // Store old bean values so we can restore them if validators fail
Map<Binding<BEAN, ?>, Object> oldValues = getBeanState(bean, Map<Binding<BEAN, ?>, Object> oldValues = getBeanState(bean,
if (binderResults.stream().anyMatch(ValidationResult::isError)) { if (binderResults.stream().anyMatch(ValidationResult::isError)) {
// Bean validator failed, revert values // Bean validator failed, revert values
restoreBeanState(bean, oldValues); restoreBeanState(bean, oldValues);
} else if (getBean() == null || bean.equals(getBean())) {
} else if (bean.equals(getBean())) {
/* /*
* Changes have been successfully saved. The set is only cleared * Changes have been successfully saved. The set is only cleared
* if using readBean/writeBean or when the changes are stored in
* the currently set bean.
*
* Writing changes to another bean when using setBean does not
* clear the set of changed bindings.
* when the changes are stored in the currently set bean.
*/
bindings.clear();
} else if (getBean() == null) {
/*
* When using readBean and writeBean there is no knowledge of
* which bean the changes come from or are stored in. Binder is
* no longer "changed" when saved succesfully to any bean.
*/ */
changedBindings.clear(); changedBindings.clear();
} }


// Generate status object and fire events. // Generate status object and fire events.
BinderValidationStatus<BEAN> status = new BinderValidationStatus<>(this, BinderValidationStatus<BEAN> status = new BinderValidationStatus<>(this,
bindingStatuses, binderResults);
bindingResults, binderResults);
getValidationStatusHandler().statusChange(status); getValidationStatusHandler().statusChange(status);
fireStatusChangeEvent(!status.isOk()); fireStatusChangeEvent(!status.isOk());
return status; return status;
Converter<FIELDVALUE, FIELDVALUE> nullRepresentationConverter = Converter Converter<FIELDVALUE, FIELDVALUE> nullRepresentationConverter = Converter
.from(fieldValue -> fieldValue, .from(fieldValue -> fieldValue,
modelValue -> Objects.isNull(modelValue) modelValue -> Objects.isNull(modelValue)
? field.getEmptyValue()
: modelValue,
? field.getEmptyValue() : modelValue,
exception -> exception.getMessage()); exception -> exception.getMessage());
ConverterDelegate<FIELDVALUE> converter = new ConverterDelegate<>( ConverterDelegate<FIELDVALUE> converter = new ConverterDelegate<>(
nullRepresentationConverter); nullRepresentationConverter);

+ 10
- 2
server/src/test/java/com/vaadin/data/BinderConverterValidatorTest.java View File



// bind a new field that has invalid value in bean // bind a new field that has invalid value in bean
TextField lastNameField = new TextField(); TextField lastNameField = new TextField();
person.setLastName("");

// The test starts with a valid value as the last name of the person,
// since the binder assumes any non-changed values to be valid.
person.setLastName("bar");

BindingBuilder<Person, String> binding2 = binder.forField(lastNameField) BindingBuilder<Person, String> binding2 = binder.forField(lastNameField)
.withValidator(notEmpty); .withValidator(notEmpty);
binding2.bind(Person::getLastName, Person::setLastName); binding2.bind(Person::getLastName, Person::setLastName);


// should not have error shown
// should not have error shown when initialized
assertNull(lastNameField.getComponentError()); assertNull(lastNameField.getComponentError());


// Set a value that breaks the validation
lastNameField.setValue("");
assertNotNull(lastNameField.getComponentError());

// add status label to show bean level error // add status label to show bean level error
Label statusLabel = new Label(); Label statusLabel = new Label();
binder.setStatusLabel(statusLabel); binder.setStatusLabel(statusLabel);

+ 32
- 4
server/src/test/java/com/vaadin/data/BinderTest.java View File

String customNullPointerRepresentation = "foo"; String customNullPointerRepresentation = "foo";
Binder<Person> binder = new Binder<>(Person.class); Binder<Person> binder = new Binder<>(Person.class);
binder.forField(nameField) binder.forField(nameField)
.withConverter(value -> value,
value -> value == null ? customNullPointerRepresentation
: value)
.withConverter(value -> value, value -> value == null
? customNullPointerRepresentation : value)
.bind("firstName"); .bind("firstName");


Person person = new Person(); Person person = new Person();
ErrorMessage errorMessage = textField.getErrorMessage(); ErrorMessage errorMessage = textField.getErrorMessage();
assertNotNull(errorMessage); assertNotNull(errorMessage);
assertEquals("foobar", errorMessage.getFormattedHtmlMessage()); assertEquals("foobar", errorMessage.getFormattedHtmlMessage());
// validation is done for the whole bean at once.
// validation is done for all changed bindings once.
assertEquals(1, invokes.get()); assertEquals(1, invokes.get());


textField.setValue("value"); textField.setValue("value");
assertEquals("Binding still affects bean even after unbind", assertEquals("Binding still affects bean even after unbind",
ageBeforeUnbind, String.valueOf(item.getAge())); ageBeforeUnbind, String.valueOf(item.getAge()));
} }

@Test
public void two_asRequired_fields_without_initial_values() {
binder.forField(nameField).asRequired("Empty name").bind(p -> "",
(p, s) -> {
});
binder.forField(ageField).asRequired("Empty age").bind(p -> "",
(p, s) -> {
});

binder.setBean(item);
assertNull("Initially there should be no errors",
nameField.getComponentError());
assertNull("Initially there should be no errors",
ageField.getComponentError());

nameField.setValue("Foo");
assertNull("Name with a value should not be an error",
nameField.getComponentError());
assertNull(
"Age field should not be in error, since it has not been modified.",
ageField.getComponentError());

nameField.setValue("");
assertNotNull("Empty name should now be in error.",
nameField.getComponentError());
assertNull("Age field should still be ok.",
ageField.getComponentError());
}
} }

Loading…
Cancel
Save