Skip to content

Commit

Permalink
feat: Add method to write only the changed properties to the bean (#1…
Browse files Browse the repository at this point in the history
…8636)

* feat: Add method to write only the changed properties to the bean

* Add unit tests for new methods

* Update test bean

* Fixes according to review

- Sanity check bindings to write in writeBean
- Return immutable Set in getBindings

* Update tests

* Formatting

---------

Co-authored-by: Teppo Kurki <teppo.kurki@vaadin.com>
  • Loading branch information
TatuLund and tepi committed Feb 12, 2024
1 parent 5f9685d commit f7e754a
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 1 deletion.
84 changes: 83 additions & 1 deletion flow-data/src/main/java/com/vaadin/flow/data/binder/Binder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2325,6 +2325,7 @@ public void refreshFields() {
* thrown.
*
* @see #writeBeanIfValid(Object)
* @see #writeBean(Object, Collection)
* @see #readBean(Object)
* @see #setBean(Object)
*
Expand All @@ -2335,13 +2336,94 @@ public void refreshFields() {
* if some of the bound field values fail to validate
*/
public void writeBean(BEAN bean) throws ValidationException {
BinderValidationStatus<BEAN> status = doWriteIfValid(bean, bindings);
writeBean(bean, bindings);
}

/**
* Writes changes from the given bindings to the given bean if all
* validators (binding and bean level) pass.
* <p>
* If any field binding validator fails, no values are written and a
* {@code ValidationException} is thrown.
* <p>
* If all field level validators pass, the given bean is updated and bean
* level validators are run on the updated bean. If any bean level validator
* fails, the bean updates are reverted and a {@code ValidationException} is
* thrown.
*
* @see #writeBeanIfValid(Object)
* @see #writeBean(Object)
* @see #readBean(Object)
* @see #setBean(Object)
* @see #writeChangedBindingsToBean(Object)
*
* @param bean
* the object to which to write the field values, not
* {@code null}
* @param bindingsToWrite
* Collection of bindings to use in writing the bean
* @throws ValidationException
* if some of the bound field values fail to validate
* @throws IllegalArgumentException
* if bindingsToWrite contains bindings not belonging to this
* Binder
*/
public void writeBean(BEAN bean,
Collection<Binding<BEAN, ?>> bindingsToWrite)
throws ValidationException {
if (!bindings.containsAll(bindingsToWrite)) {
throw new IllegalArgumentException(
"Can't write bean using binding that is not bound to this Binder.");
}
BinderValidationStatus<BEAN> status = doWriteIfValid(bean,
bindingsToWrite);
if (status.hasErrors()) {
throw new ValidationException(status.getFieldValidationErrors(),
status.getBeanValidationErrors());
}
}

/**
* Writes changes from the changed bindings to the given bean if all
* validators (binding and bean level) pass. If the bean is the same
* instance where Binder read the bean, this method updates the bean with
* the changes.
* <p>
* If any field binding validator fails, no values are written and a
* {@code ValidationException} is thrown.
* <p>
* If all field level validators pass, the given bean is updated and bean
* level validators are run on the updated bean. If any bean level validator
* fails, the bean updates are reverted and a {@code ValidationException} is
* thrown.
*
* @see #writeBeanIfValid(Object)
* @see #writeBean(Object)
* @see #readBean(Object)
* @see #setBean(Object)
*
* @param bean
* the object to which to write the field values, not
* {@code null}
* @throws ValidationException
* if some of the bound field values fail to validate
*/
public void writeChangedBindingsToBean(BEAN bean)
throws ValidationException {
writeBean(bean, getChangedBindings());
}

/**
* Get the immutable Set of changed bindings.
*
* @see #hasChanges()
*
* @return Immutable set of bindings.
*/
public Set<Binding<BEAN, ?>> getChangedBindings() {
return Collections.unmodifiableSet(changedBindings);
}

/**
* Writes successfully converted and validated changes from the bound fields
* to the bean even if there are other fields with non-validated changes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
Expand Down Expand Up @@ -443,6 +444,86 @@ public void save_bound_beanIsUpdated() throws ValidationException {
Assert.assertEquals(fieldValue, person.getFirstName());
}

@Test
public void write_binding_bound_propertyIsUpdated()
throws ValidationException {
Binder<Person> binder = new Binder<>();
Binding<Person, String> binding = binder.bind(nameField,
Person::getFirstName, Person::setFirstName);
binder.forField(ageField)
.withConverter(new StringToIntegerConverter(""))
.bind(Person::getAge, Person::setAge);

Person person = new Person();

String nameValue = "bar";
nameField.setValue(nameValue);
String ageValue = "10";
ageField.setValue(ageValue);

person.setFirstName("foo");
person.setAge(20);

binder.writeBean(person, Set.of(binding));

Assert.assertEquals(1, person.getAgeSetterCallcount());
Assert.assertEquals(nameValue, person.getFirstName());
Assert.assertNotEquals((int) Integer.valueOf(ageValue),
person.getAge());
}

@Test
public void write_changedBindings_bound_propertyIsUpdated()
throws ValidationException {
Binder<Person> binder = new Binder<>();
binder.bind(nameField, Person::getFirstName, Person::setFirstName);
binder.forField(ageField)
.withConverter(new StringToIntegerConverter(""))
.bind(Person::getAge, Person::setAge);

Person person = new Person();
Person updatedPerson = new Person();

String nameValue = "bar";
nameField.setValue(nameValue);

person.setFirstName("foo");
person.setAge(20);

Assert.assertEquals(1, binder.getChangedBindings().size());
binder.writeBean(updatedPerson, binder.getChangedBindings());

Assert.assertEquals(0, updatedPerson.getAgeSetterCallcount());
Assert.assertEquals(nameValue, updatedPerson.getFirstName());
Assert.assertEquals(0, updatedPerson.getAge());
}

@Test
public void update_bound_propertyIsUpdated() throws ValidationException {
Binder<Person> binder = new Binder<>();
binder.bind(nameField, Person::getFirstName, Person::setFirstName);
binder.forField(ageField)
.withConverter(new StringToIntegerConverter(""))
.bind(Person::getAge, Person::setAge);

Person person = new Person();
Person updatedPerson = new Person();

String nameValue = "bar";
nameField.setValue(nameValue);

person.setFirstName("foo");
person.setAge(20);

Assert.assertEquals(1, binder.getChangedBindings().size());

binder.writeChangedBindingsToBean(updatedPerson);

Assert.assertEquals(0, updatedPerson.getAgeSetterCallcount());
Assert.assertEquals(nameValue, updatedPerson.getFirstName());
Assert.assertEquals(0, updatedPerson.getAge());
}

@Test
public void save_bound_beanAsDraft() {
do_test_save_bound_beanAsDraft(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class Person implements Serializable {

private BigDecimal rent;

private int ageCallcount = 0;

public Person() {

}
Expand Down Expand Up @@ -64,6 +66,7 @@ public int getAge() {
}

public void setAge(int age) {
ageCallcount++;
this.age = age;
}

Expand Down Expand Up @@ -141,4 +144,8 @@ public static Person createTestPerson2() {
new Address("Red street", 12, "Amsterdam",
Country.NETHERLANDS));
}

public int getAgeSetterCallcount() {
return ageCallcount;
}
}

0 comments on commit f7e754a

Please sign in to comment.