vaadin / collaboration-engine

The simplest way to build real-time collaboration into web apps
https://vaadin.com/collaboration
Other
3 stars 1 forks source link

Set serialization per binding #70

Open chrosim opened 1 year ago

chrosim commented 1 year ago

At the moment it is possible to setSerializer for non "default" Classes on the CE-Binder per Class only. Let's assume we need two ComboBoxes with two different item Lists of the class CodeName. To select the departureAirport and the arrivalAirport for a FlightRoute we bind the comboboxes to the departureAirportCode and arrivalAirportCode.

public class FlightRoute{
public String departureAirportCode;
public String arrivalAirportCode;
}

public class CodeName {
public String code;
public String name;
}
  1. DepartureAirport-ComboBox
    • List departureAirports = [{code="ABC", name="Departure ABC"}]
  2. ArrivalAirport-ComboBox
    • List arrivalAirports = [{code="ABC", name="Arrival ABC"}]

It is not possible to make sure the serializer would load the correct value per binding.

As a developer i would like to enable/set serialization for a specific binding. As a alternative it would be nice to use the converter on a binding for the serialization.

Legioth commented 1 year ago

Do you consider {code="ABC", name="Arrival ABC"} to be the "same" object as {code="ABC", name="Departure ABC"}? If that's the case, then you should maybe just use "ABC" for serialization and instead let each combo box have a unique item label generator that takes care of the differences?

Alternatively, you can just treat them as distinct values since the user doesn't have any way of selecting a departure from the arrival list and vice versa.

The point of the extra serialization that is needed with Collaboration Engine is only so that when user A selects a specific value from a list, then there should be a way of letting user B also know exactly which value was selected so that they stay in sync. It's not used for cross-referencing different fields and it's not directly used for presentation.

chrosim commented 1 year ago

@Legioth No i don't consider them to be the same. My bad, i was talking about ComboBox but actualy am using MultiSelectComboBox. In my concrete case i have four MultiSelectComboBox<CodeName>'es with four different ItemLists provided by four different external api calls. I do not know the possible values of the four different List<CodeName>'s since they might change. I need to bind those MultiSelectComboBox<CodeName>'es to a Pojo storing only the code's in a list.

so I do the following for now, which is not collision safe:

List<CodeName> itemsA,itemsB,itemsC,itemsD;

public void createAndBindFields() {
    binder.setSerializer(CodeName.class, CodeName::getCode,
        value -> value == null ? null
        : Stream.concat(Stream.concat(Stream.concat(itemsA.stream(), itemsB.stream()), itemsC.stream()), itemsD.stream())
            .filter(item -> item.getValue().equals(value)).findFirst().orElse(null));

    itemsAMultiSelect = new MultiSelectComboBox<CodeName>(getTranslation("Items A"));
    itemsAMultiSelect.setItems(itemsA);
    binder.forField(itemsAMultiSelect, Set.class, CodeName.class).withConverter(createCodeNameConverter(itemsA)).bind("itemsA");

    itemsBMultiSelect = new MultiSelectComboBox<CodeName>(getTranslation("Items B"));
    itemsBMultiSelect.setItems(itemsB);
    binder.forField(itemsBMultiSelect, Set.class, CodeName.class).withConverter(createCodeNameConverter(itemsB)).bind("itemsB");

    itemsCMultiSelect = new MultiSelectComboBox<CodeName>(getTranslation("Items C"));
    itemsCMultiSelect.setItems(itemsC);
    binder.forField(itemsCMultiSelect, Set.class, CodeName.class).withConverter(createCodeNameConverter(itemsC)).bind("itemsC");

    itemsDMultiSelect = new MultiSelectComboBox<CodeName>(getTranslation("Items D"));
    itemsDMultiSelect.setItems(itemsD);
    binder.forField(itemsDMultiSelect, Set.class, CodeName.class).withConverter(createCodeNameConverter(itemsD)).bind("itemsD");
}

private Converter<Set<CodeName>, List<String>> createCodeNameConverter(Collection<CodeName> items) {
    return new Converter<Set<CodeName>, List<String>>() {

        @Override
        public Result<List<String>> convertToModel(Set<CodeName> value, ValueContext context) {
            return Result.ok(value == null ? null : value.stream().map(CodeName::getCode).collect(Collectors.toList()));
        }

        @Override
        public Set<CodeName> convertToPresentation(List<String> value, ValueContext context) {
            if (value == null)
                return null;
            return items.stream().filter(item -> value.contains(item.getCode())).collect(Collectors.toSet());
        }
    };
}
Legioth commented 1 year ago

CodeName looks like a value object for which I would suggest including all the data in the serialized form. If you can be sure that code doesn't contain special characters such as :, then you could serialize as codeName.code + ':' + codeName.name and deserialize using string.split(":", 2) and then using the two array items for finding the right instance. If you cannot have a character reserved as a separator or if you end up with more than two fields, then it might be best to use a real serialization library such as Jackson. You could also consider going further in the value object direction by treating CodeName as real value object by implementing equals and hashCode and creating new instances when deserializing instead of trying to reuse an existing instance (potentially by changing it into a record class if you're using Java 16 or newer).

Hopefully this workaround will be enough for now. We should still consider implementing the suggested feature now when the use case is understood but I'm not sure about how it would be prioritized relative to all the other improvements that we could also consider making.

chrosim commented 1 year ago

Thanks for pointing out the different way of serialization. I really did not think about it 😄 Unfortunately this workaround is not always a possibility for us, since the name value might be different for each users locale. So it's good to hear that you are considering introducing this feature.

Legioth commented 1 year ago

For that case, I guess an alternative workaround might work. You would basically need the serialized format to contain the code and a value identifying which of the four lists it belongs to (e.g. "context"). Deserialization could then use the context value to know where to look for the correct instance.

If you can modify the CodeName class, then you could add the context as an instance field there. If that's not possible, then you could add a wrapper class with one instance field for the CodeName instance and another field for the context. You would then also need to use a converter to make the field binding work and configure the combo boxes to have an item renderer that unwraps the object.