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

io.devbench.uibuilder.components.crudpanel.UIBuilderCrudPanelInterceptor Maven / Gradle / Ivy

/*
 *
 * 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.crudpanel;

import com.vaadin.flow.component.Component;
import io.devbench.uibuilder.api.components.form.UIBuilderDetailCapable;
import io.devbench.uibuilder.api.components.masterconnector.UIBuilderMasterConnector;
import io.devbench.uibuilder.api.crud.CrudControllerBean;
import io.devbench.uibuilder.api.parse.PageTransformer;
import io.devbench.uibuilder.api.parse.ParseInterceptor;
import io.devbench.uibuilder.components.crudpanel.exception.CrudPanelAmbiguousChildComponentException;
import io.devbench.uibuilder.components.crudpanel.exception.CrudPanelCannotFindMasterDetailControllerException;
import io.devbench.uibuilder.components.crudpanel.exception.CrudPanelChildNotFoundException;
import io.devbench.uibuilder.components.crudpanel.exception.CrudPanelInvalidContollerBeanException;
import io.devbench.uibuilder.components.masterdetail.UIBuilderMasterDetailController;
import io.devbench.uibuilder.components.util.datasource.DataSourceUtils;
import io.devbench.uibuilder.core.controllerbean.ControllerBeanManager;
import io.devbench.uibuilder.core.startup.ComponentTagRegistry;
import io.devbench.uibuilder.core.utils.ElementCollector;
import io.devbench.uibuilder.data.collectionds.datasource.component.AbstractDataSourceComponent;
import io.devbench.uibuilder.data.common.datasource.DataSourceChangeNotifiable;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Collectors;

import static io.devbench.uibuilder.api.crud.GenericCrudControllerBean.*;
import static io.devbench.uibuilder.api.crud.GenericItemCrudControllerBean.*;

@SuppressWarnings("unused")
public class UIBuilderCrudPanelInterceptor implements ParseInterceptor, PageTransformer {

    private static final String ITEM_SUPPLIER = "item-supplier";
    private static final String ON_ITEM_SELECTED = "on-item-selected";
    private static final String ON_SAVE = "on-save";
    private static final String ON_DELETE = "on-delete";
    private static final String ON_REFRESH = "on-refresh";
    private static final String ON_EDIT = "on-edit";
    private static final String ON_CREATE = "on-create";
    private static final String ON_CANCEL = "on-cancel";
    private static final String ON_RESET = "on-reset";
    private static final String DISABLE_MASTER_ENABLED_CONTROL = "disable-master-enabled-control";
    private static final String CONTROLLER_BEAN = "controller-bean";
    private static final String MASTER = "master";
    private static final String DETAIL = "detail";
    private static final String DATA_SOURCE = "data-source";
    private static final String ITEM_DATA_SOURCE = "item-data-source";
    private static final String MASTER_DETAIL_CONTROLLER = "master-detail-controller";
    private static final String CRUD_TOOLBAR = "crud-toolbar";
    private static final String PART = "part";
    private static final String MANAGE_NESTED = "manage-nested";
    private static final String HIDE_FORM_CONTROLS = "hide-form-controls";
    private static final String DETAIL_PANEL = "detail-panel";
    private static final String GENERIC_DATASOURCE_NAME = "generic-datasource-name";
    private static final String CRUD_PANEL = "crud-panel";
    private static final String SYNC_DETAILS_FORM_CONTROLS = "sync-details-form-controls";
    private static final String MASTER_CONNECTOR_SELECTOR = "master-connector-selector";

    @Override
    public boolean isApplicable(Element element) {
        return UIBuilderCrudPanel.TAG_NAME.equalsIgnoreCase(element.tagName());
    }

    @Override
    public void intercept(Component component, Element element) {
        Element mdc = findOneElement(element, MASTER_DETAIL_CONTROLLER).orElseThrow(CrudPanelCannotFindMasterDetailControllerException::new);
        UIBuilderCrudPanel crudPanel = (UIBuilderCrudPanel) component;
        crudPanel.setMasterDetailControllerId(mdc.id());
        ensureControllerBeanCompatibility(crudPanel, element);
    }

    private void ensureControllerBeanCompatibility(UIBuilderCrudPanel crudPanel, Element element) {
        Optional controllerBeanAttribute = elementAttr(element, CONTROLLER_BEAN);
        if (controllerBeanAttribute.isPresent()) {
            String controllerBeanName = controllerBeanAttribute.get();
            Object controllerBean = ControllerBeanManager.getInstance().getControllerBean(controllerBeanName);
            if (!(controllerBean instanceof CrudControllerBean)) {
                throw new CrudPanelInvalidContollerBeanException(
                    "Assigned controller bean (" + controllerBeanName + ") must be a " + CrudControllerBean.class.getSimpleName());
            }
        } else {
            if (element.hasAttr(GENERIC_DATASOURCE_NAME)) {
                crudPanel.registerGenericCrudData(element.attr(GENERIC_DATASOURCE_NAME));
            }
        }
    }

    @Override
    public boolean isInstantiator(Element element) {
        return true;
    }

    @Override
    public Component instantiateComponent() {
        com.vaadin.flow.dom.Element vaadinElement = new com.vaadin.flow.dom.Element(UIBuilderCrudPanel.TAG_NAME);
        return Component.from(vaadinElement, UIBuilderCrudPanel.class);
    }

    @Override
    public void transform(Element element) {
        String crudPanelId = elementId(element).orElse(UUID.randomUUID().toString());
        String masterConnectorSelector = elementAttr(element, MASTER_CONNECTOR_SELECTOR).orElse(UIBuilderMasterConnector.DEFAULT_SELECTOR);
        Element masterElement = findOneElement(element, MASTER, currentElement -> isMasterCapable(currentElement, masterConnectorSelector))
            .orElseThrow(() -> new CrudPanelChildNotFoundException("Master child not found"));

        List detailElements = element.children().stream()
            .filter(this::isDetailCapable)
            .collect(Collectors.toList());

        if (detailElements.isEmpty()) {
            throw new CrudPanelChildNotFoundException("Detail child not found");
        }

        ensureId(masterElement, crudPanelId + "-" + MASTER);
        for (int detailIdPostfix = 0; detailIdPostfix < detailElements.size(); detailIdPostfix++) {
            ensureId(detailElements.get(detailIdPostfix), crudPanelId + "-" + DETAIL + "-" + (detailIdPostfix + 1));
        }

        Optional dataSource = findOneElement(element, Arrays.asList(DATA_SOURCE, ITEM_DATA_SOURCE), "DataSource", false);

        dataSource
            .ifPresent(ds -> {
                if (DATA_SOURCE.equals(ds.tagName())) {
                    element.attr(GENERIC_DATASOURCE_NAME, ds.attr("name"));
                    masterElement.attr(GENERIC_DATASOURCE_NAME, ds.attr("name"));
                }
                DataSourceUtils.injectDataSourceId(ds);
                getChildren(element).stream()
                    .filter(this::isDataSourceCapable)
                    .filter(this::isDataSourceMissing)
                    .forEach(dsCapable -> dsCapable.appendChild(ds.clone()));
            });

        Element mdc = createMdc(element, crudPanelId, masterElement, detailElements);

        findOneElement(element, CRUD_TOOLBAR).ifPresent(toolbar -> toolbar.attr(MASTER_DETAIL_CONTROLLER, mdc.id()));

        dataSource.ifPresent(Node::remove);

        markElements(element, masterElement, detailElements);

        if (isParentManagedCrudController(element)) {
            manageNested(mdc, detailElements);
        }
    }

    private boolean isParentManagedCrudController(Element element) {
        for (Element parent : element.parents()) {
            if (UIBuilderCrudPanel.TAG_NAME.equals(parent.tagName())) {
                return parent.hasAttr(MANAGE_NESTED);
            }
        }
        return false;
    }

    private void manageNested(Element mdcElement, Collection detailElements) {
        if (!mdcElement.hasAttr(DISABLE_MASTER_ENABLED_CONTROL)) {
            mdcElement.attr(DISABLE_MASTER_ENABLED_CONTROL, true);
        }

        detailElements.forEach(detailElement -> {
            if (detailElement.tagName().equals(DETAIL_PANEL)) {
                if (!detailElement.hasAttr(HIDE_FORM_CONTROLS)) {
                    detailElement.attr(HIDE_FORM_CONTROLS, true);
                }
            }
        });
    }

    protected boolean isDataSourceCapable(Element element) {
        Predicate> isAbstractDatasource = AbstractDataSourceComponent.class::isAssignableFrom;
        Predicate> isDsChangeNotifiable = DataSourceChangeNotifiable.class::isAssignableFrom;

        return ComponentTagRegistry.getInstance()
            .getComponentClassByTag(element.tagName())
            .filter(isAbstractDatasource.or(isDsChangeNotifiable))
            .isPresent();
    }

    protected boolean isDataSourceMissing(Element element) {
        return element.children().stream().noneMatch(e -> Arrays.asList(DATA_SOURCE, ITEM_DATA_SOURCE).contains(e.tagName()));
    }

    protected boolean isMasterCapable(Element element, String masterConnectorSelector) {
        return ComponentTagRegistry.getInstance()
            .getComponentClassByTag(element.tagName())
            .map(componentClass -> UIBuilderMasterDetailController.findMasterConnector(componentClass, masterConnectorSelector))
            .filter(Optional::isPresent)
            .map(Optional::get)
            .isPresent();
    }

    protected boolean isDetailCapable(Element element) {
        return ComponentTagRegistry.getInstance()
            .getComponentClassByTag(element.tagName())
            .filter(UIBuilderDetailCapable.class::isAssignableFrom)
            .isPresent();
    }

    private String toIdList(Collection elements) {
        return elements.stream().map(Element::id).collect(Collectors.joining(","));
    }

    protected Element createMdc(Element crudPanelElement, String crudPanelId, Element masterElement, Collection detailElements) {
        Element mdc = crudPanelElement.ownerDocument().createElement(UIBuilderMasterDetailController.TAG_NAME);
        ensureId(mdc, crudPanelId + "-mdc");
        mdc.attr(MASTER, masterElement.id());
        mdc.attr(DETAIL, toIdList(detailElements));

        Optional controllerBeanAttr = elementAttr(crudPanelElement, CONTROLLER_BEAN);
        controllerBeanAttr.ifPresent(cbName -> {
            mdc.attr(ON_SAVE, cbName + "::onSave");
            mdc.attr(ON_DELETE, cbName + "::onDelete");
            mdc.attr(ON_REFRESH, cbName + "::onRefresh");
            mdc.attr(ITEM_SUPPLIER, "{{" + cbName + "::create}}");
        });

        if (!controllerBeanAttr.isPresent()) {
            if (crudPanelElement.hasAttr(GENERIC_DATASOURCE_NAME)) {
                mdc.attr(ON_SAVE, BUILT_IN_GENERIC_CRUD_PANEL_CONTROLLER_BEAN_NAME + "::onSave");
                mdc.attr(ON_DELETE, BUILT_IN_GENERIC_CRUD_PANEL_CONTROLLER_BEAN_NAME + "::onDelete");
                mdc.attr(ON_REFRESH, BUILT_IN_GENERIC_CRUD_PANEL_CONTROLLER_BEAN_NAME + "::onRefresh");
                mdc.attr(ON_CREATE, BUILT_IN_GENERIC_CRUD_PANEL_CONTROLLER_BEAN_NAME + "::onCreate");
            } else {
                mdc.attr(ON_SAVE, BUILT_IN_GENERIC_ITEM_CRUD_PANEL_CONTROLLER_BEAN_NAME + "::onSave");
                mdc.attr(ON_DELETE, BUILT_IN_GENERIC_ITEM_CRUD_PANEL_CONTROLLER_BEAN_NAME + "::onDelete");
                mdc.attr(ON_REFRESH, BUILT_IN_GENERIC_ITEM_CRUD_PANEL_CONTROLLER_BEAN_NAME + "::onRefresh");
                mdc.attr(ON_CREATE, BUILT_IN_GENERIC_ITEM_CRUD_PANEL_CONTROLLER_BEAN_NAME + "::onCreate");
            }
        }

        masterElement.attr(CRUD_PANEL, crudPanelId);
        if ("vaadin-uibuilder-grid".equals(masterElement.tagName()) && !masterElement.hasAttr("on-inline-item-saved")) {
            masterElement.attr("on-inline-item-saved", BUILT_IN_GENERIC_CRUD_PANEL_CONTROLLER_BEAN_NAME + "::handleNestedInlineItemSave");
        }

        moveAttributeToMdcIfPresent(crudPanelElement, mdc, ITEM_SUPPLIER);
        moveAttributeToMdcIfPresent(crudPanelElement, mdc, ON_ITEM_SELECTED);
        moveAttributeToMdcIfPresent(crudPanelElement, mdc, ON_SAVE);
        moveAttributeToMdcIfPresent(crudPanelElement, mdc, ON_DELETE);
        moveAttributeToMdcIfPresent(crudPanelElement, mdc, ON_REFRESH);
        moveAttributeToMdcIfPresent(crudPanelElement, mdc, ON_EDIT);
        moveAttributeToMdcIfPresent(crudPanelElement, mdc, ON_CREATE);
        moveAttributeToMdcIfPresent(crudPanelElement, mdc, ON_CANCEL);
        moveAttributeToMdcIfPresent(crudPanelElement, mdc, ON_RESET);
        moveAttributeToMdcIfPresent(crudPanelElement, mdc, DISABLE_MASTER_ENABLED_CONTROL);
        moveAttributeToMdcIfPresent(crudPanelElement, mdc, MASTER_CONNECTOR_SELECTOR);
        if (crudPanelElement.hasAttr(SYNC_DETAILS_FORM_CONTROLS)) {
            mdc.attr(SYNC_DETAILS_FORM_CONTROLS, true);
        }

        getChildren(crudPanelElement).stream()
            .filter(e -> e.tagName().toLowerCase().endsWith("-dialog"))
            .forEach(mdc::appendChild);

        crudPanelElement.appendChild(mdc);
        return mdc;
    }

    protected Optional findOneElement(Element componentElement, String tag) {
        return findOneElement(componentElement, Collections.singleton(tag), tag);
    }

    protected Optional findOneElement(Element componentElement, Collection supportedTags, String subject) {
        return findOneElement(componentElement, supportedTags, subject, true);
    }

    protected Optional findOneElement(Element componentElement, Collection supportedTags, String subject, boolean deep) {
        return supportedTags.stream()
            .map(tag -> ensureOneChild(componentElement, tag, deep))
            .filter(Optional::isPresent)
            .collect(createOnlyOneChildCollector(subject));
    }

    protected Optional findOneElement(Element componentElement, String subject, Predicate elementPredicate) {
        return componentElement.children().stream()
            .filter(elementPredicate)
            .map(Optional::of)
            .collect(createOnlyOneChildCollector(subject));
    }

    protected void markElements(Element crudElement, Element masterElement, Collection detailElements) {
        masterElement.attr(PART, MASTER);
        detailElements.forEach(detailElement -> detailElement.attr(PART, DETAIL));
    }

    private void moveAttributeToMdcIfPresent(Element crudPanel, Element mdc, String attrName) {
        elementAttr(crudPanel, attrName).ifPresent(value -> mdc.attr(attrName, value));
    }

    private Optional elementAttr(Element element, String attrName) {
        if (element.hasAttr(attrName)) {
            String value = element.attr(attrName);
            return value.trim().isEmpty() ? Optional.empty() : Optional.of(value);
        }
        return Optional.empty();
    }

    private Optional elementId(Element element) {
        return elementAttr(element, ElementCollector.ID);
    }

    private void ensureId(Element element, String idIfMissing) {
        Optional id = elementId(element);
        if (!id.isPresent()) {
            element.attr(ElementCollector.ID, idIfMissing);
        }
    }

    private Optional ensureOneChild(Element element, String childTagName, boolean deep) {
        List sourceElements = deep ? getChildren(element) : element.children();
        List elements = sourceElements.stream().filter(e -> childTagName.equalsIgnoreCase(e.tagName())).collect(Collectors.toList());
        if (elements.size() > 1) {
            throw new CrudPanelAmbiguousChildComponentException(
                "Multiple instance (" + elements.size() + ") of child element '" + childTagName + "' is not allowed");
        } else if (elements.size() == 1) {
            return Optional.of(elements.get(0));
        }
        return Optional.empty();
    }

    @SuppressWarnings("unchecked")
    private Collector, Object, Optional> createOnlyOneChildCollector(String subject) {
        return Collectors.collectingAndThen((Collector, Object, List>>) Collectors.>toList(),
            elements -> {
                if (elements.isEmpty()) {
                    return Optional.empty();
                } else if (elements.size() > 1) {
                    String tags = elements.stream().map(Optional::get).map(Element::tagName).collect(Collectors.joining(", "));
                    throw new CrudPanelAmbiguousChildComponentException("Multiple components of type '" + subject + "' are not supported yet: " + tags);
                }
                return elements.get(0);
            });
    }

    private List getChildren(Element root) {
        List elements = new ArrayList<>();
        for (Element child : root.children()) {
            if (!UIBuilderCrudPanel.TAG_NAME.equalsIgnoreCase(child.tagName())) {
                elements.add(child);
                if (!child.children().isEmpty()) {
                    elements.addAll(getChildren(child));
                }
            }
        }
        return elements;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy