io.devbench.uibuilder.spring.crud.SpringGenericItemCrudControllerBean Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of uibuilder-spring Show documentation
Show all versions of uibuilder-spring Show documentation
Spring support for the UIBuilder Framework
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.spring.crud;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.spring.annotation.UIScope;
import io.devbench.uibuilder.annotations.ControllerBean;
import io.devbench.uibuilder.api.components.HasRawElementComponent;
import io.devbench.uibuilder.api.components.form.UIBuilderDetailCapable;
import io.devbench.uibuilder.api.components.masterconnector.UIBuilderMasterConnector;
import io.devbench.uibuilder.api.controllerbean.uieventhandler.Item;
import io.devbench.uibuilder.api.controllerbean.uieventhandler.Source;
import io.devbench.uibuilder.api.controllerbean.uieventhandler.UIEventHandler;
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.member.scanner.MemberScanner;
import io.devbench.uibuilder.core.parse.elementwalker.ElementWalker;
import io.devbench.uibuilder.core.utils.reflection.ClassMetadata;
import io.devbench.uibuilder.core.utils.reflection.PropertyMetadata;
import io.devbench.uibuilder.spring.crud.exception.GenericItemCrudControllerException;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import javax.persistence.OneToMany;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
@Slf4j
@UIScope
@ControllerBean(GenericItemCrudControllerBean.BUILT_IN_GENERIC_ITEM_CRUD_PANEL_CONTROLLER_BEAN_NAME)
public class SpringGenericItemCrudControllerBean implements GenericItemCrudControllerBean {
public static final String ITEM_BIND = "item-bind";
public static final String ITEM_BIND_ITEMS_PREFIX = "items:";
public static final String ITEM_DATA_SOURCE = "item-data-source";
private final Map> joinItemSuppliers;
public SpringGenericItemCrudControllerBean() {
this.joinItemSuppliers = new HashMap<>();
}
@Override
public void registerJoinItemSupplier(@NotNull String mdcId, @Nullable Supplier> joinItemSupplier) {
joinItemSuppliers.put(mdcId, joinItemSupplier);
}
@Override
@UIEventHandler("onRefresh")
public void refresh(@Source Refreshable refreshable) {
if (refreshable != null) {
refreshable.refresh();
}
}
@Override
@UIEventHandler("onSave")
public void onSave(@Item TYPE subject, @Source Refreshable refreshable) {
if (asItemCreateProvider(refreshable).isSelectedItemCreated()) {
asMasterConnectorProvider(refreshable).getMasterConnector().addItem(subject);
}
refreshable.refresh();
}
@Override
@UIEventHandler("onDelete")
public void onDelete(@Item TYPE subject, @Source Refreshable refreshable) {
// if the master connector is direct modifiable, then the remove on member list will be done automatically by the MDC
}
@Override
@UIEventHandler("onCreate")
public void onCreate(@Source Refreshable refreshable) {
CanCreate itemCreateProvider = asItemCreateProvider(refreshable);
UIBuilderMasterConnector extends Component, TYPE> masterConnector = asMasterConnectorProvider(refreshable).getMasterConnector();
Component masterComponent = masterConnector.getMasterComponent();
if (masterComponent instanceof HasRawElementComponent) {
Element masterElement = ((HasRawElementComponent) masterComponent).getRawElement();
findReferencedParentCollectionPropertyName(masterElement)
.ifPresent(parentItemCollectionPropertyName -> findParentMdcId(masterElement)
.ifPresent(parentMdcId -> handleOneToManyRelation(itemCreateProvider, parentItemCollectionPropertyName, parentMdcId)));
}
}
private Optional findParentMdcId(Element currentMasterElement) {
AtomicReference foundParentMdcId = new AtomicReference<>();
currentMasterElement.parents().stream()
.filter(this::isElementDetailCapable)
.findFirst()
.map(Element::id)
.ifPresent(detailPanelId -> {
Element rootElement = (Element) currentMasterElement.root();
ElementWalker.of(rootElement)
.withProcessor(element -> {
if ("master-detail-controller".equals(element.normalName()) && element.attr("detail").trim().equals(detailPanelId)) {
foundParentMdcId.set(element.id());
}
return new Elements(element);
}).walk();
});
return Optional.ofNullable(foundParentMdcId.get());
}
private boolean isElementDetailCapable(Element element) {
return MemberScanner.getInstance().findClassesBySuperType(UIBuilderDetailCapable.class).stream()
.filter(it -> it.isAnnotationPresent(Tag.class))
.map(it -> it.getAnnotation(Tag.class).value())
.anyMatch(it -> it.equalsIgnoreCase(element.normalName()));
}
@SuppressWarnings("unchecked")
private MasterConnectorProvider asMasterConnectorProvider(Refreshable refreshable) {
if (refreshable instanceof MasterConnectorProvider) {
return (MasterConnectorProvider) refreshable;
}
throw new GenericItemCrudControllerException("Source is not a master connector provider");
}
@SuppressWarnings("unchecked")
private CanCreate asItemCreateProvider(Refreshable refreshable) {
if (refreshable instanceof CanCreate) {
return (CanCreate) refreshable;
}
throw new GenericItemCrudControllerException("Source is not an item create provider");
}
private Optional findReferencedParentCollectionPropertyName(Element masterElement) {
String masterComponentItemBind = masterElement.attr(ITEM_BIND).trim();
if (masterComponentItemBind.startsWith(ITEM_BIND_ITEMS_PREFIX)) {
return Optional.of(masterComponentItemBind.substring(ITEM_BIND_ITEMS_PREFIX.length()));
} else {
return masterElement.children().stream()
.filter(element -> ITEM_DATA_SOURCE.equals(element.normalName()))
.filter(element -> element.hasAttr(ITEM_BIND))
.filter(element -> element.attr(ITEM_BIND).trim().startsWith(ITEM_BIND_ITEMS_PREFIX))
.map(element -> element.attr(ITEM_BIND).trim().substring(ITEM_BIND_ITEMS_PREFIX.length()))
.findFirst();
}
}
private void handleOneToManyRelation(CanCreate itemCreateProvider, String parentItemCollectionPropertyName, String parentMdcId) {
Supplier> parentSelectedItemSupplier = joinItemSuppliers.get(parentMdcId);
if (parentSelectedItemSupplier != null) {
Object parentSelectedItem = parentSelectedItemSupplier.get();
ClassMetadata> parentSelectedItemClassMetadata = ClassMetadata.ofValue(parentSelectedItem);
parentSelectedItemClassMetadata.getProperties().stream()
.filter(propertyMetadata -> parentItemCollectionPropertyName.equals(propertyMetadata.getName()))
.filter(propertyMetadata -> Collection.class.isAssignableFrom(propertyMetadata.getType()))
.filter(propertyMetadata -> propertyMetadata.getParameterizedType().getActualTypeArguments().length == 1)
.findFirst()
.ifPresent(propertyMetadata -> handleOneToManyRelationPropertyValues(
itemCreateProvider, parentSelectedItem, parentSelectedItemClassMetadata, propertyMetadata));
}
}
private void handleOneToManyRelationPropertyValues(CanCreate itemCreateProvider, Object parentSelectedItem,
ClassMetadata> parentSelectedItemClassMetadata, PropertyMetadata> propertyMetadata) {
Type itemType = propertyMetadata.getParameterizedType().getActualTypeArguments()[0];
TYPE newItem = instantiateNewItem(itemType);
if (propertyMetadata.isAnnotationPresent(OneToMany.class)) {
String newItemJoinPropertyName = propertyMetadata.getAnnotation(OneToMany.class).mappedBy();
if (!newItemJoinPropertyName.isEmpty()) { // else, try to find unique field that matches the type?
PropertyMetadata newItemJoinPropertyMetadata = ClassMetadata.ofValue(newItem).getProperties().stream()
.filter(newItemPropertyMetadata -> newItemPropertyMetadata.getName().equals(newItemJoinPropertyName))
.filter(newItemPropertyMetadata -> newItemPropertyMetadata.getType().isAssignableFrom(parentSelectedItemClassMetadata.getTargetClass()))
.findFirst()
.orElseThrow(() -> new GenericItemCrudControllerException(
"Could not find join field in class " + itemType.getTypeName() + " by property name: " + newItemJoinPropertyName));
newItemJoinPropertyMetadata.setValue(parentSelectedItem);
}
}
itemCreateProvider.createItem(newItem);
}
private TYPE instantiateNewItem(Type itemType) {
try {
@SuppressWarnings("unchecked")
TYPE newItem = ((Class) itemType).getConstructor().newInstance();
return newItem;
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new GenericItemCrudControllerException("Could not instantiate new item from type " + itemType.getTypeName(), e);
}
}
}