io.devbench.uibuilder.components.masterdetail.UIBuilderMasterDetailController Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of uibuilder-master-detail-controller Show documentation
Show all versions of uibuilder-master-detail-controller Show documentation
A Master-Detail Controller component for the UIBuilder Framework
/*
*
* 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 extends Component, T> 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 extends Component> 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 extends UIBuilderMasterConnector> masterConnectorClass) {
try {
Constructor extends UIBuilderMasterConnector> 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 extends Component, T> 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 extends Component> 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 extends Component, T> 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 extends Component, T> getMasterConnector() {
return masterConnector;
}
@SuppressWarnings("unchecked")
protected UIBuilderMasterConnector findMasterConnector() {
Class extends Component> 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();
}
}