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

io.devbench.uibuilder.components.masterdetail.UIBuilderMasterDetailController Maven / Gradle / Ivy

There is a newer version: 3.0.2
Show 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.masterdetail;

import com.vaadin.flow.component.*;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.dom.DisabledUpdateMode;
import com.vaadin.flow.shared.Registration;
import elemental.json.Json;
import elemental.json.JsonNull;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
import io.devbench.uibuilder.api.components.HasItemType;
import io.devbench.uibuilder.api.components.HasRawElement;
import io.devbench.uibuilder.api.components.HasRawElementComponent;
import io.devbench.uibuilder.api.components.masterconnector.UIBuilderMasterConnector;
import io.devbench.uibuilder.api.controllerbean.uieventhandler.EventQualifier;
import io.devbench.uibuilder.api.crud.CanCreate;
import io.devbench.uibuilder.api.crud.GenericItemCrudControllerBean;
import io.devbench.uibuilder.api.crud.MasterConnectorProvider;
import io.devbench.uibuilder.api.crud.Refreshable;
import io.devbench.uibuilder.api.exceptions.ComponentException;
import io.devbench.uibuilder.api.exceptions.ComponentInternalException;
import io.devbench.uibuilder.api.exceptions.ProgressInterruptException;
import io.devbench.uibuilder.api.listeners.BackendAttachListener;
import io.devbench.uibuilder.api.member.scanner.MemberScanner;
import io.devbench.uibuilder.components.masterdetail.connector.GridMasterConnector;
import io.devbench.uibuilder.components.masterdetail.event.*;
import io.devbench.uibuilder.components.masterdetail.exception.MasterConnectorNotFoundException;
import io.devbench.uibuilder.components.masterdetail.exception.MasterDetailControllerInterruptException;
import io.devbench.uibuilder.components.masterdetail.internal.EventHandlerResult;
import io.devbench.uibuilder.components.masterdetail.internal.FlagAwareContext;
import io.devbench.uibuilder.components.masterdetail.internal.InternalEventHandlerResult;
import io.devbench.uibuilder.core.controllerbean.ControllerBeanManager;
import io.devbench.uibuilder.core.controllerbean.UIEventHandlerContext;
import io.devbench.uibuilder.core.controllerbean.statenodemanager.StateNodeManager;
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 lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Provider;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static io.devbench.uibuilder.api.controllerbean.uieventhandler.EventQualifier.*;
import static io.devbench.uibuilder.components.masterdetail.UIBuilderMasterDetailControllerEventHandlerMethod.*;

@Tag(UIBuilderMasterDetailController.TAG_NAME)
@JsModule("./uibuilder-master-detail-controller/uibuilder-master-detail-controller.js")
public class UIBuilderMasterDetailController extends HasRawElementComponent
    implements HasElement, HasRawElement, BackendAttachListener, Refreshable, CanCreate, MasterConnectorProvider, HasItemType {

    public static final String TAG_NAME = "master-detail-controller";
    public static final String DEFAULT_DETAIL_PROPERTY = "item";
    private static final Logger log = LoggerFactory.getLogger(UIBuilderMasterDetailController.class);
    private static final com.vaadin.flow.component.PropertyDescriptor PROP_DISABLE_MASTER_ENABLED_CONTROL =
        PropertyDescriptors.propertyWithDefault("disableMasterEnabledControl", false);

    private static final com.vaadin.flow.component.PropertyDescriptor PROP_ITEM_SUPPLIER =
        PropertyDescriptors.propertyWithDefault("itemSupplier", "");

    @SuppressWarnings({"unchecked", "rawtypes"})
    private static final PropertyConverter SUPPLIER_CONVERTER =
        (PropertyConverter) PropertyConverters.getConverterByType(Supplier.class);

    private static final String EVENT_DETAIL_PROPERTY_NAME = "event.detail.propertyName";
    private static final String EVENT_DETAIL_OLD_VALUE = "event.detail.oldValue";
    private static final String EVENT_DETAIL_NEW_VALUE = "event.detail.newValue";
    private static final String EVENT_DETAIL_ERROR_MESSAGE = "event.detail.errorMessage";

    @Inject
    private Provider> genericItemCrudControllerBeanProvider;

    private Collection selectedItems;

    private String masterId;
    private Map detailIds = new HashMap<>();

    @Getter
    @Setter
    private String masterConnectorSelector = UIBuilderMasterConnector.DEFAULT_SELECTOR;

    @Setter(AccessLevel.PROTECTED)
    private HtmlElementAwareComponent master;
    private Set details = new HashSet<>();
    private UIBuilderMasterDetailController parentMdc;

    private UIBuilderMasterConnector masterConnector;
    private Registration masterConnectorSelectionChangedRegistration;

    private boolean created = false;
    private boolean detailDirty = false;
    private final Map methodNameMap = new HashMap<>();
    private String parentMdcId;

    private Class itemType;

    private static boolean isInstantiable(Class clz) {
        return !Modifier.isAbstract(clz.getModifiers())
            && !Modifier.isInterface(clz.getModifiers());
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    public static Optional findMasterConnector(Class masterComponentClass, String selector) {
        return MemberScanner.getInstance().findClassesBySuperType(UIBuilderMasterConnector.class)
            .stream()
            .filter(UIBuilderMasterDetailController::isInstantiable)
            .map(UIBuilderMasterDetailController::tryToInstantiate)
            .filter(Objects::nonNull)
            .filter(masterConnector -> masterConnector.isApplicable(masterComponentClass, selector))
            .min(Comparator.comparing(UIBuilderMasterConnector::getPriority));
    }

    @SuppressWarnings("rawtypes")
    private static UIBuilderMasterConnector tryToInstantiate(Class masterConnectorClass) {
        try {
            Constructor constructor = masterConnectorClass.getDeclaredConstructor();
            return constructor.newInstance();
        } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) {
            log.debug("Could not instantiate master connector: " + masterConnectorClass.getName(), e);
            return null;
        }
    }

    public void registerIntoGenericItemControllerBean() {
        String id = getId().orElseThrow(() -> new ComponentInternalException("Cannot register MDC into generic item controller, no component ID found"));
        try {
            GenericItemCrudControllerBean genericItemCrudControllerBean = genericItemCrudControllerBeanProvider.get();
            genericItemCrudControllerBean.registerJoinItemSupplier(id, this::getSelectedItem);
        } catch (Exception e) {
            log.warn("Generic item controller bean instance cannot be found", e);
        }
    }

    public void markDetailDirty() {
        getElement().callJsFunction("detailMarkDirty");
    }

    @Override
    public boolean isSelectedItemCreated() {
        return created;
    }

    public boolean addToMasterList(T item) {
        UIBuilderMasterConnector masterConnector = getMasterConnector();
        if (masterConnector.isDirectModifiable()) {
            masterConnector.addItem(item);
            masterConnector.refresh();
            return true;
        }
        return false;
    }

    private Optional getDetailPropertyWriteMethod(Component component) {
        return details.stream()
            .filter(detail -> detail.getComponent() == component)
            .map(detail -> {
                String detailProperty = detailIds.get(detail.getElement().id());
                Class componentClass = component.getClass();
                try {
                    return Arrays
                        .stream(Introspector.getBeanInfo(componentClass, Introspector.USE_ALL_BEANINFO).getPropertyDescriptors())
                        .filter(pd -> Objects.equals(pd.getName(), detailProperty))
                        .map(PropertyDescriptor::getWriteMethod)
                        .filter(Objects::nonNull)
                        .findFirst();
                } catch (IntrospectionException e) {
                    log.warn("No write method found for detail component's property " + detailProperty, e);
                }
                return Optional.empty();
            })
            .filter(Optional::isPresent)
            .map(Optional::get)
            .map(Method.class::cast)
            .findFirst();
    }

    @SuppressWarnings("rawtypes")
    private Collection getDetailComponentAsMDCEvenListener() {
        return details.stream()
            .map(HtmlElementAwareComponent::getComponent)
            .filter(MasterDetailControllerEventListener.class::isInstance)
            .map(MasterDetailControllerEventListener.class::cast)
            .collect(Collectors.toSet());
    }

    @Override
    public void onAttached() {
        ElementCollector.getById(masterId, this).ifPresent(
            htmlElementAwareComponent -> {
                master = htmlElementAwareComponent;
                wireMasterComponent();
            }
        );

        details = new HashSet<>();
        detailIds.forEach((detailId, detailProperty) -> {
            ElementCollector.getById(detailId, this).ifPresent(
                htmlElementAwareComponent -> {
                    details.add(htmlElementAwareComponent);
                    wireDetailComponent(htmlElementAwareComponent);
                }
            );
        });
        wireDetailRelatedEvents();

        if (parentMdcId != null) {
            ElementCollector.getById(parentMdcId)
                .ifPresent(parentMdcComponent -> parentMdc = parentMdcComponent.getComponent());
        }

        setupMdcRefreshEvent();
        setupMdcDeleteEvent();
        setupMdcCreateEvent();
        setupMdcEditEvent();
        setupMdcErrorHandler();

        setupMdcDetailEnabledEvent("detail-enabled", true);
        setupMdcDetailEnabledEvent("detail-disabled", false);

        getElement().callJsFunction("_onAttached");
    }

    private void setupMdcErrorHandler() {
        getElement().addEventListener("handle-error", e -> {
            final String errorMessage;
            if (e.getEventData().hasKey(EVENT_DETAIL_ERROR_MESSAGE)) {
                errorMessage = e.getEventData().getString(EVENT_DETAIL_ERROR_MESSAGE);
            } else {
                errorMessage = null;
            }
            throw new MasterDetailControllerInterruptException(errorMessage);
        }).addEventData(EVENT_DETAIL_ERROR_MESSAGE);
    }

    private void setupMdcDetailEnabledEvent(String eventName, boolean detailEnabled) {
        getElement().addEventListener(eventName, e -> changeDetailEnabled(detailEnabled));
    }

    private void setupMdcEditEvent() {
        getElement().addEventListener("edit",
            e -> callEventHandlerMethod(EDIT, true)
                .onError(this::showErrorOnFrontend)
                .onProgress(() -> {
                    changeMasterEnabled(false);
                    changeDetailEnabled(true);
                    getDetailComponentAsMDCEvenListener().forEach(listener -> listener.edit(getSelectedItem()));
                }));
    }

    private void setupMdcCreateEvent() {
        getElement().addEventListener("create", e -> {
                selectedItems = new ArrayList<>();
                created = false;
                callEventHandlerMethod(CREATE, REQUESTED, true, null, null)
                    .onError(this::showErrorOnFrontend)
                    .onProgress(() -> {
                        if (getSelectedItem() == null) {
                            Supplier itemSupplier = getItemSupplier();
                            if (itemSupplier != null) {
                                T createdItem = itemSupplier.get();
                                setSelectedItem(createdItem, true);
                            }
                        }
                        if (getSelectedItem() != null && isSelectedItemCreated()) {
                            getDetailComponentAsMDCEvenListener().forEach(listener -> listener.create(getSelectedItem()));
                        }
                    });
            }
        );
    }

    private void setupMdcDeleteEvent() {
        getElement().addEventListener("delete", e -> callEventHandlerMethod(DELETE, true)
            .onProgress(() -> {
                UIBuilderMasterConnector masterConnector = getMasterConnector();
                if (masterConnector.isDirectModifiable()) {
                    masterConnector.removeItems(getSelectedItems());
                    masterConnector.refresh();
                }
                setSelectedItem(null);
                masterConnector.setSelectedItem(null);
                getElement().callJsFunction("_onValueChanged", "", "", "");
            })
            .onError(this::showErrorOnFrontend));
    }

    private void setupMdcRefreshEvent() {
        getElement().addEventListener("refresh", e -> callEventHandlerMethod(REFRESH, true)
            .onProgress(() -> getMasterConnector().refresh())
            .onError(this::showErrorOnFrontend));
    }

    @Override
    protected void onDetach(DetachEvent detachEvent) {
        if (masterConnectorSelectionChangedRegistration != null) {
            masterConnectorSelectionChangedRegistration.remove();
            masterConnectorSelectionChangedRegistration = null;
        }
    }

    private EventHandlerResult fireItemSelectionRequested(Object item) {
        boolean selected = item != null && (!(item instanceof Collection) || ((Collection) item).size() == 1);
        return callEventHandlerMethod(ITEM_SELECTED, REQUESTED, false, item, event -> ((ItemSelectedEvent) event).setItemSelected(selected));
    }

    @Override
    public UIBuilderMasterConnector getMasterConnector() {
        return masterConnector;
    }

    @SuppressWarnings("unchecked")
    protected UIBuilderMasterConnector findMasterConnector() {
        Class masterComponentClass = master.getComponent().getClass();
        return findMasterConnector(masterComponentClass, masterConnectorSelector)
            .orElseThrow(() -> new MasterConnectorNotFoundException(masterComponentClass, masterConnectorSelector));
    }

    private void wireMasterComponent() {
        FlagAwareContext errorAwareContext = new FlagAwareContext();
        masterConnector = findMasterConnector();
        masterConnector.connect(master.getComponent());
        masterConnectorSelectionChangedRegistration = masterConnector
            .addSelectionChangedListener(event -> {
                if (!created) {
                    Collection newlySelectedItems = event.getNewlySelectedItems();
                    Collection previouslySelectedItems = event.getPreviouslySelectedItems();

                    errorAwareContext.progress(() -> fireItemSelectionRequested(newlySelectedItems)
                        .onProgress(() -> setSelectedItems(newlySelectedItems))
                        .onError(e -> {
                            errorAwareContext.run(() -> masterConnector.setSelectedItems(previouslySelectedItems));
                            showErrorOnFrontend(e);
                        }));
                }
            });
    }

    @SuppressWarnings("UnusedReturnValue")
    private EventHandlerResult fireEditEvent(EventQualifier qualifier, boolean asDefault, String propertyName, String oldValue, String newValue) {
        return callEventHandlerMethod(EDIT, qualifier, asDefault, getSelectedItems(), event -> {
            EditEvent editEvent = (EditEvent) event;
            editEvent.setPropertyName(propertyName);
            editEvent.setOldValue(oldValue);
            editEvent.setNewValue(newValue);
        });
    }

    private void wireDetailComponent(HtmlElementAwareComponent detail) {
        Component detailComponent = detail.getComponent();

        setupDetailComponentValueSetterEvent(detailComponent);
        setupDetailComponentValueChangeEvent(detailComponent);
        setupDetailComponentReadyEvent(detailComponent);
    }

    private void wireDetailRelatedEvents() {
        setupMdcSaveEvent();
        setupMdcResetEvent();
        setupMdcCancelEvent();

        changeDetailEnabled(false);
    }

    private void setupMdcCancelEvent() {
        getElement().addEventListener("cancel", event -> callEventHandlerMethod(CANCEL, true)
            .onError(this::showErrorOnFrontend)
            .onProgress(this::baseCancelEventHandler));
    }

    private void setupMdcResetEvent() {
        getElement().addEventListener("reset", event -> callEventHandlerMethod(RESET, true)
            .onError(this::showErrorOnFrontend)
            .onProgress(() -> {
                if (!created) {
                    changeMasterEnabled(true);
                }
                detailDirty = false;
                getDetailComponentAsMDCEvenListener().forEach(MasterDetailControllerEventListener::reset);
            }));
    }

    private void setupMdcSaveEvent() {
        getElement().addEventListener("save", event -> callEventHandlerMethod(SAVE, true)
            .onError(this::showErrorOnFrontend)
            .onProgress(() -> {
                getElement().callJsFunction("_onSave");
                changeMasterEnabled(true);
                detailDirty = false;
                T selectedItem = getSelectedItem();
                getDetailComponentAsMDCEvenListener().forEach(listener -> listener.save(selectedItem));
                getMasterConnector().setSelectedItem(selectedItem);
                created = false;
            }));
    }

    private void setupDetailComponentReadyEvent(Component detailComponent) {
        detailComponent.getElement().addEventListener("detail-ready",
            event -> callEventHandlerMethod(ITEM_SELECTED, DETAIL_READY, true, getSelectedItems(),
                componentEvent -> {
                    Collection selectedItems = getSelectedItems();
                    ((ItemSelectedEvent) componentEvent).setCreated(isSelectedItemCreated());
                    ((ItemSelectedEvent) componentEvent).setItemSelected(selectedItems != null && !selectedItems.isEmpty());
                }));
    }

    private void setupDetailComponentValueChangeEvent(Component detailComponent) {
        detailComponent.getElement()
            .addEventListener("value-changed", event -> {
                JsonObject eventData = event.getEventData();
                String propertyName = getNullableJsonValueAsString(eventData, EVENT_DETAIL_PROPERTY_NAME);
                String oldValue = getNullableJsonValueAsString(eventData, EVENT_DETAIL_OLD_VALUE);
                String newValue = getNullableJsonValueAsString(eventData, EVENT_DETAIL_NEW_VALUE);
                getElement().callJsFunction("_onValueChanged", propertyName, oldValue, newValue);
                changeMasterEnabled(false);
                if (!detailDirty) {
                    fireEditEvent(MODIFICATION_STARTED, true, propertyName, oldValue, newValue);
                    if (masterConnector instanceof GridMasterConnector) {
                        ((GridMasterConnector) masterConnector).applyCustomFix();
                    }
                    detailDirty = true;
                }

                fireEditEvent(FORM_FIELD_VALUE_CHANGED, false, propertyName, oldValue, newValue);
            }).addEventData(EVENT_DETAIL_PROPERTY_NAME)
            .addEventData(EVENT_DETAIL_OLD_VALUE)
            .addEventData(EVENT_DETAIL_NEW_VALUE);
    }

    private void setupDetailComponentValueSetterEvent(Component detailComponent) {
        getDetailPropertyWriteMethod(detailComponent)
            .ifPresent(method -> getElement()
                .addEventListener("selected-item-changed", event -> {
                    T selectedItem = getSelectedItem();
                    try {
                        try {
                            method.invoke(detailComponent, selectedItem);
                            callEventHandlerMethod(ITEM_SELECTED, DETAIL_ITEM_SET, false, selectedItems, componentEvent -> {
                                ((ItemSelectedEvent) componentEvent).setCreated(isSelectedItemCreated());
                                ((ItemSelectedEvent) componentEvent).setItemSelected(selectedItem != null);
                            });
                            if (selectedItem != null && isSelectedItemCreated()) {
                                masterConnector.setSelectedItem(selectedItem);
                                changeMasterEnabled(false);
                                changeDetailEnabled(true);
                                getElement().callJsFunction("detailMarkNewItem");
                            }
                        } catch (IllegalArgumentException e) {
                            log.error("Incompatible setter method with detail component", e);
                        }
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        throw new ComponentException("Could not invoke method: "
                            + method.getName() + " on detail component: "
                            + detailComponent.getClass().getSimpleName()
                            + "(" + detailComponent.getId().orElse("no ID") + ")", e);
                    }
                    changeDetailEnabled(selectedItem != null);
                }));
    }

    private void baseCancelEventHandler() {
        changeMasterEnabled(true);
        setSelectedItem(null);
        detailDirty = false;
        details.forEach(detail -> {
            if (detail.getComponent() instanceof MasterDetailControllerEventListener) {
                ((MasterDetailControllerEventListener) detail.getComponent()).cancel();
            }
        });
        getMasterConnector().setSelectedItems(Collections.emptyList());
    }

    void showErrorOnFrontend(ProgressInterruptException progressInterrupted) {
        if (progressInterrupted.isShowErrorMessageOnFrontend()) {
            JsonObject messageObj = Json.createObject();
            messageObj.put("message", Objects.requireNonNull(progressInterrupted.getMessage()));
            getElement().callJsFunction("_onError", messageObj);
        }
    }

    private String getNullableJsonValueAsString(JsonObject o, String key) {
        if (o.hasKey(key)) {
            JsonValue value = o.get(key);
            if (value != null && !(value instanceof JsonNull)) {
                return value.asString();
            }
        }
        return null;
    }

    private void changeMasterEnabled(boolean enabled) {
        if (master.getComponent() instanceof HasEnabled && !isMasterEnabledControlDisabled()) {
            HasEnabled masterComponent = master.getComponent();
            boolean currentEnabled = masterComponent.isEnabled();
            if (currentEnabled != enabled) {
                masterComponent.setEnabled(enabled);
                getElement().callJsFunction("_onMasterEnabledChange", enabled);
            }
        }
    }

    private void changeDetailEnabled(boolean enabled) {
        getDetailComponents().forEach(component -> {
            if (component instanceof HasEnabled) {
                ((HasEnabled) component).setEnabled(enabled);
            }
        });
    }

    @Synchronize(property = "disableMasterEnabledControl", value = {"attached", "disable-master-enabled-control-changed"})
    public boolean isMasterEnabledControlDisabled() {
        return get(PROP_DISABLE_MASTER_ENABLED_CONTROL);
    }

    public void setMasterEnabledControlDisabled(boolean masterEnabledControlDisabled) {
        set(PROP_DISABLE_MASTER_ENABLED_CONTROL, masterEnabledControlDisabled);
    }

    @SuppressWarnings("unchecked")
    @Synchronize(property = "itemSupplier", value = {"attached", "selected-item-changed"}, allowUpdates = DisabledUpdateMode.ALWAYS)
    public Supplier getItemSupplier() {
        String supplierId = get(PROP_ITEM_SUPPLIER);
        return StringUtils.isNotBlank(supplierId) ? SUPPLIER_CONVERTER.convertFrom(supplierId) : null;
    }

    public void setItemSupplier(Supplier supplier) {
        Objects.requireNonNull(supplier, "Supplier cannot be null");
        set(PROP_ITEM_SUPPLIER, SUPPLIER_CONVERTER.convertTo(supplier));
    }

    public boolean isOnlyOneItemSelected() {
        return selectedItems != null && selectedItems.size() == 1;
    }

    public T getSelectedItem() {
        return isOnlyOneItemSelected() ? selectedItems.iterator().next() : null;
    }

    public void setSelectedItem(T item) {
        setSelectedItem(item, false);
    }

    private void setSelectedItem(T item, boolean created) {
        setSelectedItems(item == null ? null : Collections.singleton(item), created);
    }

    public Collection getSelectedItems() {
        return selectedItems;
    }

    public void setSelectedItems(Collection selectedItems) {
        setSelectedItems(selectedItems, false);
    }

    private void setSelectedItems(Collection selectedItems, boolean created) {
        this.created = created;
        this.selectedItems = selectedItems;
        boolean selected = selectedItems != null && !selectedItems.isEmpty();
        callEventHandlerMethod(ITEM_SELECTED, MASTER_DETAIL_CONTROLLER_ITEM_SET, false, selectedItems, event -> {
            ((ItemSelectedEvent) event).setCreated(created);
            ((ItemSelectedEvent) event).setItemSelected(selected);
        })
            .onError(this::showErrorOnFrontend)
            .onProgress(() -> getElement().callJsFunction("_fireSelectedItemChanged", selected, created));
    }

    @Override
    public void createItem(T item) {
        setSelectedItem(item, true);
    }

    @Override
    public Class getItemType() {
        return itemType;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void setItemType(Class itemType) {
        this.itemType = (Class) itemType;
    }

    public Optional> findParentMdc() {
        return Optional.ofNullable(parentMdc);
    }

    public void setParentMdcId(String parentMdcId) {
        this.parentMdcId = parentMdcId;
    }

    public String getMasterId() {
        return masterId;
    }

    public void setMasterId(String masterId) {
        this.masterId = masterId;
    }

    /**
     * @return the ID of the detail component, or the first detail component's ID, if multiple present
     */
    public String getDetailId() {
        return detailIds.isEmpty() ? null : detailIds.keySet().iterator().next();
    }

    public Map getDetailIdPropertyMap() {
        return detailIds;
    }

    public void setDetailIds(String... detailIds) {
        this.detailIds = Arrays.stream(detailIds)
            .map(this::createDetailIdDetailPropertyEntry)
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    private Map.Entry createDetailIdDetailPropertyEntry(String detailIdAndProperty) {
        String trimmedDetailIdAndProperty = detailIdAndProperty.trim();
        if (trimmedDetailIdAndProperty.indexOf(":") > 0) {
            String[] detailIdAndPropertySplit = trimmedDetailIdAndProperty.split(":");
            if (detailIdAndPropertySplit.length == 2) {
                String detailId = detailIdAndPropertySplit[0].trim();
                String detailProperty = detailIdAndPropertySplit[1].trim();
                return new AbstractMap.SimpleEntry<>(detailId, detailProperty);
            }
        }
        return new AbstractMap.SimpleEntry<>(trimmedDetailIdAndProperty, DEFAULT_DETAIL_PROPERTY);
    }

    public void setDetailProperty(String detailProperty) {
        new HashSet<>(detailIds.keySet()).forEach(detailId -> detailIds.put(detailId, detailProperty));
    }

    public Optional getMasterComponent() {
        return Optional.ofNullable(master != null ? master.getComponent() : null);
    }

    public Optional getDetailComponent() {
        return getDetailComponents().stream().findFirst();
    }

    public Collection getDetailComponents() {
        return details.stream()
            .map(HtmlElementAwareComponent::getComponent)
            .map(Component.class::cast)
            .collect(Collectors.toSet());
    }

    public void registerEventHandlerMethodName(UIBuilderMasterDetailControllerEventHandlerMethod method, String methodName) {
        this.methodNameMap.put(method, methodName);
    }

    private EventHandlerResult callEventHandlerMethod(UIBuilderMasterDetailControllerEventHandlerMethod method) {
        return callEventHandlerMethod(method, false);
    }

    private EventHandlerResult callEventHandlerMethod(UIBuilderMasterDetailControllerEventHandlerMethod method, boolean asDefault) {
        return callEventHandlerMethod(method, REQUESTED, asDefault, getSelectedItems(), null);
    }

    protected ControllerBeanManager getControllerBeanManager() {
        return ControllerBeanManager.getInstance();
    }

    private EventHandlerResult callEventHandlerMethod(UIBuilderMasterDetailControllerEventHandlerMethod method, EventQualifier qualifier,
                                                      boolean asDefault, Object selectedItems, Consumer eventModifier) {

        ProgressInterruptException progressInterruptException = null;

        String methodName = methodNameMap.get(method);
        if (methodName != null) {
            ControllerBeanManager controllerBeanManager = getControllerBeanManager();
            String qualifiedName = controllerBeanManager.getEvenHandlerNormalizedNameToQualifiedNameMap().get(methodName);
            if (qualifiedName == null) {
                throw new ComponentInternalException("Could not find qualified name for: " + methodName);
            }

            Optional context = controllerBeanManager.getEventHandlerContext(qualifiedName, qualifier);
            if (!context.isPresent() && asDefault) {
                context = controllerBeanManager.getEventHandlerContext(qualifiedName);
            }

            try {
                context.ifPresent(eventHandlerContext -> callEventHandler(method, eventHandlerContext, selectedItems));
            } catch (ProgressInterruptException e) {
                progressInterruptException = e;
            }
        }

        QualifierAwareComponentEvent componentEvent = method.createEvent(this, qualifier);
        if (eventModifier != null) {
            eventModifier.accept(componentEvent);
        }
        if (progressInterruptException == null) {
            try {
                ComponentUtil.fireEvent(this, componentEvent);
            } catch (ProgressInterruptException e) {
                progressInterruptException = e;
            }
        }

        return InternalEventHandlerResult.create(progressInterruptException);
    }

    private void callEventHandler(UIBuilderMasterDetailControllerEventHandlerMethod method, UIEventHandlerContext eventHandlerContext, Object item) {
        if (method == SAVE) {
            wrapInStateNodeManagerSync(() -> eventHandlerContext.callEventHandlerWithItem(item, this));
        } else {
            eventHandlerContext.callEventHandlerWithItem(item, this);
        }
    }

    private void wrapInStateNodeManagerSync(Runnable runnable) {
        Optional pageStateNodeManager = getPageStateNodeManager();
        pageStateNodeManager.ifPresent(StateNodeManager::synchronizeProperties);
        runnable.run();
        pageStateNodeManager.ifPresent(StateNodeManager::synchronizeStateNodes);
    }

    private Optional getPageStateNodeManager() {
        Optional parent = getParent();
        while (parent.isPresent()) {
            Component parentComponent = parent.get();
            if (parentComponent instanceof Page) {
                return Optional.ofNullable(((Page) parentComponent).getStateNodeManager());
            }
            parent = parentComponent.getParent();
        }
        return Optional.empty();
    }

    @SuppressWarnings("UnusedReturnValue")
    public Registration addItemSelectedEventListener(ComponentEventListener itemSelectedEventListener) {
        return addListener(ItemSelectedEvent.class, itemSelectedEventListener);
    }

    @SuppressWarnings("UnusedReturnValue")
    public Registration addEditEventListener(ComponentEventListener editEventListener) {
        return addListener(EditEvent.class, editEventListener);
    }

    @SuppressWarnings("UnusedReturnValue")
    public Registration addCreateEventListener(ComponentEventListener createEventListener) {
        return addListener(CreateEvent.class, createEventListener);
    }

    @SuppressWarnings("UnusedReturnValue")
    public Registration addDeleteEventListener(ComponentEventListener deleteEventListener) {
        return addListener(DeleteEvent.class, deleteEventListener);
    }

    @SuppressWarnings("UnusedReturnValue")
    public Registration addRefreshEventListener(ComponentEventListener refreshEventListener) {
        return addListener(RefreshEvent.class, refreshEventListener);
    }

    @SuppressWarnings("UnusedReturnValue")
    public Registration addSaveEventListener(ComponentEventListener saveEventListener) {
        return addListener(SaveEvent.class, saveEventListener);
    }

    @SuppressWarnings("UnusedReturnValue")
    public Registration addResetEventListener(ComponentEventListener resetEventListener) {
        return addListener(ResetEvent.class, resetEventListener);
    }

    @SuppressWarnings("UnusedReturnValue")
    public Registration addCancelEventListener(ComponentEventListener cancelEventListener) {
        return addListener(CancelEvent.class, cancelEventListener);
    }

    @SuppressWarnings("UnusedReturnValue")
    public Registration addMasterEnabledChangedEventListener(ComponentEventListener masterEnabledChangedEventListener) {
        return addListener(MasterEnabledChangedEvent.class, masterEnabledChangedEventListener);
    }

    @Override
    public void refresh() {
        getMasterConnector().refresh();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy