All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.devbench.uibuilder.components.multivalue.UIBuilderMultiValue Maven / Gradle / Ivy

The newest version!
/*
 *
 * Copyright © 2018 Webvalto Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.devbench.uibuilder.components.multivalue;

import com.vaadin.flow.component.*;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.data.binder.HasItems;
import com.vaadin.flow.dom.DisabledUpdateMode;
import com.vaadin.flow.function.SerializableBiFunction;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.internal.nodefeature.ElementData;
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
import io.devbench.uibuilder.api.components.HasItemType;
import io.devbench.uibuilder.api.listeners.BackendAttachListener;
import io.devbench.uibuilder.api.utils.PropertyPath;
import io.devbench.uibuilder.components.crudpanel.UIBuilderCrudPanel;
import io.devbench.uibuilder.components.multivalue.exception.IllegalMultiValueItemInstantiationException;
import io.devbench.uibuilder.components.multivalue.exception.IllegalMultiValueItemsStateException;
import io.devbench.uibuilder.core.controllerbean.uiproperty.PropertyConverter;
import io.devbench.uibuilder.core.controllerbean.uiproperty.PropertyConverters;
import io.devbench.uibuilder.core.page.Page;
import io.devbench.uibuilder.core.utils.ElementCollector;
import io.devbench.uibuilder.core.utils.HtmlElementAwareComponent;
import io.devbench.uibuilder.core.utils.JsonSerializer;
import io.devbench.uibuilder.core.utils.reflection.ClassMetadata;
import io.devbench.uibuilder.core.utils.reflection.PropertyMetadata;
import io.devbench.uibuilder.data.collectionds.datasource.component.AbstractDataSourceComponent;
import lombok.AccessLevel;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static io.devbench.uibuilder.components.multivalue.UIBuilderMultiValueParseInterceptor.*;

@Slf4j
@Tag("uibuilder-multi-value")
@JsModule("./uibuilder-multi-value/src/uibuilder-multi-value.js")
public class UIBuilderMultiValue extends AbstractSinglePropertyField, List> implements
    HasItems, HasComponents, HasItemType, BackendAttachListener {

    private static final String PROP_NAME_ITEM_SUPPLIER = "itemSupplier";
    private static final String PROP_NAME_VALUE_ITEM_PATH = "valueItemPath";
    private static final String EVENT_DETAIL_INDEX = "event.detail.index";
    private static final String EVENT_DETAIL_TEMPLATE_HTML_CONTENT = "event.detail.templateHtmlContent";
    private static final String EVENT_DETAIL_INNER_BINDINGS = "event.detail.innerBindings";
    private static final String EVENT_DETAIL_INNER_BINDINGS_ELEMENT_ID_MAP = "event.detail.innerBindingsElementIdMap";
    private static final String EVENT_DETAIL_PROPERTY_PATH = "event.detail.propertyPath";
    private static final String EVENT_DETAIL_PROPERTY_VALUE = "event.detail.propertyValue";
    private static final String EVENT_DETAIL_PROPERTY_ELEMENT_ID = "event.detail.propertyElementId";
    private static final String EVENT_DETAIL_USER_ADDED = "event.detail.userAdded";
    private static final com.vaadin.flow.component.PropertyDescriptor PROP_ITEM_SUPPLIER =
        PropertyDescriptors.propertyWithDefault(PROP_NAME_ITEM_SUPPLIER, "");
    private static final com.vaadin.flow.component.PropertyDescriptor PROP_VALUE_ITEM_PATH =
        PropertyDescriptors.propertyWithDefault(PROP_NAME_VALUE_ITEM_PATH, DEFAULT_VALUE_ITEM_PATH);
    @SuppressWarnings({"unchecked", "rawtypes"})
    private static final PropertyConverter SUPPLIER_CONVERTER =
        (PropertyConverter) PropertyConverters.getConverterByType(Supplier.class);
    @Setter(AccessLevel.PACKAGE)
    private UIBuilderMultiValueBindingContext bindingContext;
    @Setter(AccessLevel.PACKAGE)
    private String parentCrudElementId;
    @Setter(AccessLevel.PACKAGE)
    private String parentCrudElementBindingPropertyPath;
    private List items;
    private Class itemType;

    public UIBuilderMultiValue() {
        super("value", new ArrayList<>(), JsonArray.class,
            (SerializableBiFunction, JsonArray, List>) (component, presentation) ->
                PropertyConverters.convertToObjectList(component.getValueType(), JsonSerializer.toObjects(String.class, presentation)),
            (SerializableBiFunction, List, JsonArray>) (component, list) ->
                JsonSerializer.toJson(PropertyConverters.convertToStringList(list)));
    }

    @Synchronize(property = PROP_NAME_VALUE_ITEM_PATH, value = {"value-changed"}, allowUpdates = DisabledUpdateMode.ALWAYS)
    public String getValueItemPath() {
        return get(PROP_VALUE_ITEM_PATH);
    }

    public void setValueItemPath(String valueItemPath) {
        set(PROP_VALUE_ITEM_PATH, valueItemPath);
    }

    @SuppressWarnings("unchecked")
    @Synchronize(property = PROP_NAME_ITEM_SUPPLIER, value = {"instance-added"}, allowUpdates = DisabledUpdateMode.ALWAYS)
    public Supplier getItemSupplier() {
        String supplierId = get(PROP_ITEM_SUPPLIER);
        return StringUtils.isNotBlank(supplierId) ? SUPPLIER_CONVERTER.convertFrom(supplierId) : createDefaultItemSupplier();
    }

    public void setItemSupplier(Supplier supplier) {
        set(PROP_ITEM_SUPPLIER, SUPPLIER_CONVERTER.convertTo(supplier != null ? supplier : createDefaultItemSupplier()));
    }

    private Supplier createDefaultItemSupplier() {
        return () -> {
            if (hasItemType()) {
                try {
                    return this.getItemType().getDeclaredConstructor().newInstance();
                } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                    throw new IllegalMultiValueItemInstantiationException("Could not create item instance", e);
                }
            } else {
                return null;
            }
        };
    }

    public  List getValues(Class valueType) {
        return PropertyConverters.convertToObjectList(valueType,
            getValue().stream().map(t -> t == null ? null : t.toString()).collect(Collectors.toList()));
    }

    @SuppressWarnings("unchecked")
    public Class getItemType() {
        if (itemType == null && parentCrudElementId != null && parentCrudElementBindingPropertyPath != null) {
            Class crudClassType = UIBuilderCrudPanel.findCrudClassType(parentCrudElementId);
            if (crudClassType != null) {
                ClassMetadata classMetadata = ClassMetadata.ofClass(crudClassType);
                Optional> foundProperty = classMetadata.property(parentCrudElementBindingPropertyPath);
                foundProperty.flatMap(PropertyMetadata::getFirstParameterizedType).ifPresent(paramType -> itemType = paramType);

            }
        }
        return (Class) itemType;
    }

    public void setItemType(Class itemType) {
        this.itemType = itemType;
    }

    @SuppressWarnings("unchecked")
    Class getValueType() {
        if (getItemType() != null) {
            ClassMetadata itemTypeMetadata = ClassMetadata.ofClass(getItemType());
            String valueItemPath = getValueItemPath();
            Optional> foundValueItemPathProperty = itemTypeMetadata.property(valueItemPath);
            if (foundValueItemPathProperty.isPresent()) {
                return (Class) foundValueItemPathProperty.get().getType();
            }
        }
        return (Class) String.class;
    }

    private boolean hasItemType() {
        return getItemType() != null;
    }

    @Override
    public void onAttached() {
        attachInstanceAddedEvent();
        attachInstanceRemovedEvent();
        attachInstanceChangedEvent();
    }

    private void attachInstanceChangedEvent() {
        getElement().addEventListener("instance-changed", event -> {
            if (hasItemType()) {
                JsonObject eventData = event.getEventData();
                int index = (int) eventData.getNumber(EVENT_DETAIL_INDEX);

                if (index >= getItems().size()) {
                    throw new IllegalMultiValueItemsStateException(index, getItems().size());
                }

                PropertyPath propertyPath = new PropertyPath(eventData.getString(EVENT_DETAIL_PROPERTY_PATH));
                if (!propertyPath.isSingleLevel()) {
                    String propertyValue = JsonSerializer.toObject(String.class, eventData.get(EVENT_DETAIL_PROPERTY_VALUE));
                    String path = propertyPath.subPath(1).toString();

                    T item = getItems().get(index);

                    ClassMetadata itemMetadata = ClassMetadata.ofValue(item);
                    itemMetadata.getProperties().stream().filter(propertyMetadata -> propertyMetadata.getName().equals(path)).findFirst()
                        .ifPresent(propertyMetadata -> {
                            handleBackendComponentValueChange(eventData.getString(EVENT_DETAIL_PROPERTY_ELEMENT_ID),
                                propertyMetadata::setValue,
                                () -> {
                                    try {
                                        @SuppressWarnings("unchecked")
                                        PropertyConverter converterFor =
                                            (PropertyConverter) PropertyConverters.getConverterFor(propertyMetadata);
                                        Object value = converterFor.convertFrom(propertyValue);
                                        propertyMetadata.setValue(value);
                                    } catch (Exception e) {
                                        log.warn("Could not set illegal property ({}) value ({}) for item (index: {})", path, propertyValue, index, e);
                                    }
                                });
                        });
                }
            }

        }).addEventData(EVENT_DETAIL_INDEX)
            .addEventData(EVENT_DETAIL_PROPERTY_PATH)
            .addEventData(EVENT_DETAIL_PROPERTY_VALUE)
            .addEventData(EVENT_DETAIL_PROPERTY_ELEMENT_ID);
    }

    private  void handleBackendComponentValueChange(String elementId, Consumer backendValue, Runnable nonBackendValue) {
        @SuppressWarnings("unchecked")
        Optional> hasValue = Optional.ofNullable(elementId)
            .flatMap(ElementCollector::getById)
            .map(HtmlElementAwareComponent::getComponent)
            .filter(HasValue.class::isInstance)
            .map(HasValue.class::cast);
        if (hasValue.isPresent()) {
            backendValue.accept(hasValue.get().getValue());
        } else {
            nonBackendValue.run();
        }
    }

    private void attachInstanceRemovedEvent() {
        getElement().addEventListener("instance-removed", event -> {
            if (hasItemType()) {
                JsonObject eventData = event.getEventData();
                int index = (int) eventData.getNumber(EVENT_DETAIL_INDEX);
                List items = getItems();
                if (items.size() > index) {
                    items.remove(index);
                }
                removeAttachedVirtualChildrenByIndex(index);
                bindingContext.removeBackendComponentValueSuppliersAndConsumers(index);
            }
        }).addEventData(EVENT_DETAIL_INDEX);
    }

    private synchronized void removeAttachedVirtualChildrenByIndex(int index) {
        String indexSuffix = "-index-" + index;
        VirtualChildrenList virtualChildrenList = getElement().getNode().getFeature(VirtualChildrenList.class);
        List stateNodeToRemove = new ArrayList<>();
        virtualChildrenList.forEachChild(stateNode -> {
            ElementData elementData = stateNode.getFeature(ElementData.class);
            String id = getIdFromElementDataPayload(elementData);
            if (id != null && id.endsWith(indexSuffix)) {
                stateNodeToRemove.add(stateNode);
            }
        });

        stateNodeToRemove.forEach(stateNode -> {
            int stateNodeIndex = virtualChildrenList.indexOf(stateNode);
            virtualChildrenList.remove(stateNodeIndex);
        });
    }

    private String getIdFromElementDataPayload(@NotNull ElementData elementData) {
        JsonValue payload = elementData.getPayload();
        if (payload instanceof JsonObject) {
            JsonObject payloadObject = (JsonObject) payload;
            if (payloadObject.hasKey("type") && payloadObject.hasKey("payload") && payloadObject.getString("type").equals("@id")) {
                return payloadObject.getString("payload");
            }
        }
        return null;
    }

    private void attachInstanceAddedEvent() {
        getElement().addEventListener("instance-added", event -> {
            JsonObject eventData = event.getEventData();
            int index = (int) eventData.getNumber(EVENT_DETAIL_INDEX);
            boolean userAdded = eventData.getBoolean(EVENT_DETAIL_USER_ADDED);

            String templateHtmlContent = eventData.getString(EVENT_DETAIL_TEMPLATE_HTML_CONTENT);
            List innerBindings = JsonSerializer.toObjects(String.class, eventData.getArray(EVENT_DETAIL_INNER_BINDINGS));
            Page.findComponentParentPage(this)
                .ifPresent(parentPage -> parentPage.processFragment(templateHtmlContent, this, innerBindings));


            T pushItem = null;
            if (userAdded) {
                T newItem = getItemSupplier().get();
                if (hasItemType()) {
                    getItems().add(index, newItem);
                }
                pushItem = newItem;
            } else {
                if (hasItemType()) {
                    pushItem = getItems().get(index);
                }
            }

            registerPossibleBackendComponentValueSuppliers(index, eventData.getObject(EVENT_DETAIL_INNER_BINDINGS_ELEMENT_ID_MAP));
            if (pushItem != null) {
                getElement().callJsFunction("_pushItemValue", index, itemToJson(index, pushItem));
            }
        }).addEventData(EVENT_DETAIL_USER_ADDED)
            .addEventData(EVENT_DETAIL_INDEX)
            .addEventData(EVENT_DETAIL_TEMPLATE_HTML_CONTENT)
            .addEventData(EVENT_DETAIL_INNER_BINDINGS)
            .addEventData(EVENT_DETAIL_INNER_BINDINGS_ELEMENT_ID_MAP);
    }

    @SuppressWarnings("unchecked")
    private void registerPossibleBackendComponentValueSuppliers(int index, JsonObject innerBindingsElementIdMap) {
        for (String propertyPath : innerBindingsElementIdMap.keys()) {
            String elementId = innerBindingsElementIdMap.getString(propertyPath);
            if (StringUtils.isNotBlank(elementId)) {
                ElementCollector.getById(elementId)
                    .map(HtmlElementAwareComponent::getComponent)
                    .filter(HasValue.class::isInstance)
                    .map(HasValue.class::cast)
                    .ifPresent(hasValue -> {

                        Supplier valueSupplier = hasValue instanceof AbstractDataSourceComponent
                            ? createDataSourceComponentValueSupplier(hasValue)
                            : hasValue::getValue;

                        Consumer valueInitializer = item -> {
                            ClassMetadata.ofValue(item)
                                .property(new PropertyPath(propertyPath).level(1).toString())
                                .ifPresent(propertyMetadata -> hasValue.setValue(propertyMetadata.getValue()));
                        };

                        bindingContext.registerBackendComponentValueSupplier(index, propertyPath, valueSupplier, valueInitializer);
                    });
            }
        }
    }

    @SuppressWarnings("unchecked")
    private 

Supplier createDataSourceComponentValueSupplier(HasValue hasValue) { AbstractDataSourceComponent

component = (AbstractDataSourceComponent

) hasValue; return () -> { P value = (P) hasValue.getValue(); if (value == null) { return null; } else { return component .mapDataSource( ds -> ds.getDataProcessor().getKeyMapper().getKey(ClassMetadata.ofValue(value)), null) .orElse(null); } }; } public List getItems() { if (items == null) { items = new ArrayList<>(); } return items; } /** * Sets the items to handle by the component * * @param items is a collection of the items to handle. If items is a List, the list will be passed directly, * and the original List instance will be handled. If the list is an other type of collection, * for example a Set, the set will be wrapped in a list, and the original collection is untouched, * the modifications will NOT be reflected to the original instance. */ public void setItems(Collection items) { if (items instanceof List) { this.items = (List) items; } else if (items == null) { this.items = new ArrayList<>(); } else { this.items = new ArrayList<>(items); } getElement().callJsFunction("setItems", itemsToJson(this.items)); } private JsonValue itemsToJson(List items) { JsonArray itemsJson = Json.createArray(); for (int i = 0; i < items.size(); i++) { itemsJson.set(i, itemToJson(i, items.get(i))); } return itemsJson; } private JsonObject itemToJson(int index, T item) { if (item == null) { JsonObject object = Json.createObject(); object.put(bindingContext.getTemplateBeanName(), Json.createObject()); return object; } else { return bindingContext.createJsonItem(index, item, getValueItemPath()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy