
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