/* * Copyright 2016 Red Hat, Inc. and/or its affiliates. * * 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 org.drools.workbench.screens.guided.dtable.client.editor; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.PostConstruct; import javax.enterprise.context.Dependent; import javax.enterprise.event.Event; import javax.enterprise.event.Observes; import javax.inject.Inject; import com.google.gwt.core.client.Scheduler; import com.google.gwt.user.client.ui.IsWidget; import org.drools.workbench.models.datamodel.imports.Imports; import org.drools.workbench.models.guided.dtable.shared.model.GuidedDecisionTable52; import org.drools.workbench.screens.guided.dtable.client.editor.menu.EditMenuBuilder; import org.drools.workbench.screens.guided.dtable.client.editor.menu.InsertMenuBuilder; import org.drools.workbench.screens.guided.dtable.client.editor.menu.RadarMenuBuilder; import org.drools.workbench.screens.guided.dtable.client.editor.menu.ViewMenuBuilder; import org.drools.workbench.screens.guided.dtable.client.type.GuidedDTableGraphResourceType; import org.drools.workbench.screens.guided.dtable.client.widget.table.GuidedDecisionTableModellerView; import org.drools.workbench.screens.guided.dtable.client.widget.table.GuidedDecisionTablePresenter; import org.drools.workbench.screens.guided.dtable.client.widget.table.GuidedDecisionTablePresenter.Access; import org.drools.workbench.screens.guided.dtable.client.widget.table.GuidedDecisionTableView; import org.drools.workbench.screens.guided.dtable.client.widget.table.events.cdi.DecisionTableSelectedEvent; import org.drools.workbench.screens.guided.dtable.client.wizard.NewGuidedDecisionTableWizardHelper; import org.drools.workbench.screens.guided.dtable.model.GuidedDecisionTableEditorContent; import org.drools.workbench.screens.guided.dtable.model.GuidedDecisionTableEditorGraphContent; import org.drools.workbench.screens.guided.dtable.model.GuidedDecisionTableEditorGraphModel; import org.drools.workbench.screens.guided.dtable.service.GuidedDecisionTableEditorService; import org.drools.workbench.screens.guided.dtable.service.GuidedDecisionTableGraphEditorService; import org.guvnor.common.services.project.context.ProjectContext; import org.guvnor.common.services.shared.metadata.model.Metadata; import org.guvnor.common.services.shared.metadata.model.Overview; import org.jboss.errai.bus.client.api.messaging.Message; import org.jboss.errai.common.client.api.Caller; import org.jboss.errai.common.client.api.RemoteCallback; import org.jboss.errai.ioc.client.container.SyncBeanManager; import org.kie.workbench.common.widgets.client.callbacks.CommandDrivenErrorCallback; import org.kie.workbench.common.widgets.client.datamodel.AsyncPackageDataModelOracle; import org.kie.workbench.common.widgets.client.popups.validation.ValidationPopup; import org.uberfire.backend.vfs.ObservablePath; import org.uberfire.backend.vfs.Path; import org.uberfire.client.annotations.WorkbenchEditor; import org.uberfire.client.annotations.WorkbenchMenu; import org.uberfire.client.annotations.WorkbenchPartTitle; import org.uberfire.client.annotations.WorkbenchPartView; import org.uberfire.client.callbacks.Callback; import org.uberfire.client.mvp.LockManager; import org.uberfire.client.mvp.LockTarget; import org.uberfire.client.mvp.PlaceManager; import org.uberfire.client.mvp.SaveInProgressEvent; import org.uberfire.client.mvp.UpdatedLockStatusEvent; import org.uberfire.client.workbench.events.ChangeTitleWidgetEvent; import org.uberfire.ext.editor.commons.client.menu.MenuItems; import org.uberfire.ext.editor.commons.client.resources.i18n.CommonConstants; import org.uberfire.ext.editor.commons.version.events.RestoreEvent; import org.uberfire.ext.widgets.common.client.callbacks.DefaultErrorCallback; import org.uberfire.ext.widgets.common.client.callbacks.HasBusyIndicatorDefaultErrorCallback; import org.uberfire.lifecycle.OnClose; import org.uberfire.lifecycle.OnFocus; import org.uberfire.lifecycle.OnMayClose; import org.uberfire.lifecycle.OnStartup; import org.uberfire.mvp.Command; import org.uberfire.mvp.ParameterizedCommand; import org.uberfire.mvp.PlaceRequest; import org.uberfire.mvp.impl.PathPlaceRequest; import org.uberfire.workbench.events.NotificationEvent; import org.uberfire.workbench.model.menu.Menus; import static org.drools.workbench.screens.guided.dtable.client.widget.table.GuidedDecisionTablePresenter.Access.LockedBy.CURRENT_USER; import static org.drools.workbench.screens.guided.dtable.client.widget.table.GuidedDecisionTablePresenter.Access.LockedBy.NOBODY; import static org.drools.workbench.screens.guided.dtable.client.widget.table.GuidedDecisionTablePresenter.Access.LockedBy.OTHER_USER; import static org.uberfire.client.annotations.WorkbenchEditor.LockingStrategy.EDITOR_PROVIDED; import static org.uberfire.ext.widgets.common.client.common.ConcurrentChangePopup.newConcurrentDelete; import static org.uberfire.ext.widgets.common.client.common.ConcurrentChangePopup.newConcurrentRename; import static org.uberfire.ext.widgets.common.client.common.ConcurrentChangePopup.newConcurrentUpdate; /** * Guided Decision Table Graph Editor Presenter */ @Dependent @WorkbenchEditor(identifier = "GuidedDecisionTableGraphEditor", supportedTypes = {GuidedDTableGraphResourceType.class}, lockingStrategy = EDITOR_PROVIDED) public class GuidedDecisionTableGraphEditorPresenter extends BaseGuidedDecisionTableEditorPresenter { private final Caller<GuidedDecisionTableGraphEditorService> graphService; private final Event<SaveInProgressEvent> saveInProgressEvent; private final LockManager lockManager; protected ObservablePath.OnConcurrentUpdateEvent concurrentUpdateSessionInfo = null; protected Access access = new Access(); protected Integer originalGraphHash; private GuidedDecisionTableEditorGraphContent content; private LoadGraphLatch loadGraphLatch = null; private SaveGraphLatch saveGraphLatch = null; private ProjectContext context; private NewGuidedDecisionTableWizardHelper helper; @Inject public GuidedDecisionTableGraphEditorPresenter(final View view, final Caller<GuidedDecisionTableEditorService> service, final Caller<GuidedDecisionTableGraphEditorService> graphService, final Event<NotificationEvent> notification, final Event<SaveInProgressEvent> saveInProgressEvent, final Event<DecisionTableSelectedEvent> decisionTableSelectedEvent, final ValidationPopup validationPopup, final GuidedDTableGraphResourceType dtGraphResourceType, final EditMenuBuilder editMenuBuilder, final ViewMenuBuilder viewMenuBuilder, final InsertMenuBuilder insertMenuBuilder, final RadarMenuBuilder radarMenuBuilder, final GuidedDecisionTableModellerView.Presenter modeller, final ProjectContext context, final NewGuidedDecisionTableWizardHelper helper, final SyncBeanManager beanManager, final PlaceManager placeManager, final LockManager lockManager) { super(view, service, notification, decisionTableSelectedEvent, validationPopup, dtGraphResourceType, editMenuBuilder, viewMenuBuilder, insertMenuBuilder, radarMenuBuilder, modeller, beanManager, placeManager); this.graphService = graphService; this.saveInProgressEvent = saveInProgressEvent; this.context = context; this.helper = helper; this.lockManager = lockManager; } @PostConstruct public void init() { super.init(); //Selecting a Decision Table in the document selector fires a selection event registeredDocumentsMenuBuilder.setActivateDocumentCommand((document) -> { final GuidedDecisionTablePresenter dtPresenter = ((GuidedDecisionTablePresenter) document); decisionTableSelectedEvent.fire(new DecisionTableSelectedEvent(dtPresenter)); }); //Removing a Decision Table from the document selector is equivalent to closing the editor registeredDocumentsMenuBuilder.setRemoveDocumentCommand((document) -> { final GuidedDecisionTablePresenter dtPresenter = ((GuidedDecisionTablePresenter) document); if (mayClose(dtPresenter)) { removeDocument(dtPresenter); } }); registeredDocumentsMenuBuilder.setNewDocumentCommand(this::onNewDocument); } void onNewDocument() { final Path contextPath = context.getActivePackage().getPackageMainResourcesPath(); helper.createNewGuidedDecisionTable(contextPath, "", GuidedDecisionTable52.TableFormat.EXTENDED_ENTRY, GuidedDecisionTable52.HitPolicy.NONE, view, (path) -> onOpenDocumentsInEditor(Collections.singletonList(path))); } @Override @OnStartup public void onStartup(final ObservablePath path, final PlaceRequest placeRequest) { super.onStartup(path, placeRequest); initialiseEditor(path, placeRequest); } @Override @OnFocus public void onFocus() { super.onFocus(); } @Override public void loadDocument(final ObservablePath path, final PlaceRequest placeRequest) { throw new UnsupportedOperationException(); } void initialiseEditor(final ObservablePath path, final PlaceRequest placeRequest) { this.access.setReadOnly(placeRequest.getParameter("readOnly", null) != null); initialiseLockManager(); initialiseVersionManager(); addFileChangeListeners(path); loadDocumentGraph(path); } void initialiseVersionManager() { versionRecordManager.init(null, editorPath, (versionRecord) -> { versionRecordManager.setVersion(versionRecord.id()); access.setReadOnly(!versionRecordManager.isLatest(versionRecord)); registeredDocumentsMenuBuilder.setReadOnly(isReadOnly()); reload(); }); } void loadDocumentGraph(final ObservablePath path) { view.showLoading(); view.refreshTitle(getTitleText()); graphService.call(getLoadGraphContentSuccessCallback(), getNoSuchFileExceptionErrorCallback()).loadContent(path); } private RemoteCallback<GuidedDecisionTableEditorGraphContent> getLoadGraphContentSuccessCallback() { return (content) -> { this.content = content; this.originalGraphHash = content.getModel().hashCode(); this.concurrentUpdateSessionInfo = null; final GuidedDecisionTableEditorGraphModel model = content.getModel(); final Set<GuidedDecisionTableEditorGraphModel.GuidedDecisionTableGraphEntry> modelEntries = model.getEntries(); initialiseEditorTabsWhenNoDocuments(); if (modelEntries == null || modelEntries.isEmpty()) { view.hideBusyIndicator(); return; } loadGraphLatch = new LoadGraphLatch(modelEntries.size(), getSelectDecisionTableCommand(modelEntries.iterator().next().getPathHead()), () -> originalGraphHash = buildModelFromEditor().hashCode()); modelEntries.stream().forEach(loadGraphLatch::loadDocumentGraphEntry); }; } private ParameterizedCommand<GuidedDecisionTableView.Presenter> getSelectDecisionTableCommand(final Path dtToSelectPath) { return (dtPresenter) -> { if (dtPresenter.getCurrentPath().getOriginal().equals(dtToSelectPath)) { decisionTableSelectedEvent.fire(new DecisionTableSelectedEvent(dtPresenter, false)); } }; } PathPlaceRequest getPathPlaceRequest(final Path path) { return new PathPlaceRequest(path); } void initialiseLockManager() { lockManager.init(new LockTarget(editorPath, view.asWidget(), editorPlaceRequest, () -> editorPath.getFileName() + " - " + resourceType.getDescription(), () -> {/*nothing*/})); } @Override @WorkbenchPartTitle public String getTitleText() { return versionRecordManager.getCurrentPath().getFileName() + " - " + resourceType.getDescription(); } @Override @WorkbenchPartView public IsWidget getWidget() { return super.getWidget(); } @Override @WorkbenchMenu public Menus getMenus() { return super.getMenus(); } @Override @OnMayClose public boolean mayClose() { setMayCloseHandler(this::doMayCloseGraph); boolean mayClose = mayClose(originalGraphHash, buildModelFromEditor().hashCode()); setMayCloseHandler(this::doMayCloseDocument); mayClose = mayClose && super.mayClose(); return mayClose; } private boolean doMayCloseGraph(final Integer originalHashCode, final Integer currentHashCode) { if (this.isDirty(originalHashCode, currentHashCode) || overviewWidget.isDirty()) { return this.editorView.confirmClose(); } return true; } private boolean doMayCloseDocument(final Integer originalHashCode, final Integer currentHashCode) { if (this.isDirty(originalHashCode, currentHashCode)) { return this.editorView.confirmClose(); } return true; } GuidedDecisionTableEditorGraphModel buildModelFromEditor() { final GuidedDecisionTableEditorGraphModel model = new GuidedDecisionTableEditorGraphModel(); for (GuidedDecisionTableView.Presenter dtPresenter : modeller.getAvailableDecisionTables()) { model.getEntries().add(new GuidedDecisionTableEditorGraphModel.GuidedDecisionTableGraphEntry(dtPresenter.getLatestPath(), dtPresenter.getCurrentPath(), dtPresenter.getView().getX(), dtPresenter.getView().getY())); } return model; } @Override @OnClose public void onClose() { lockManager.releaseLock(); super.onClose(); } @Override protected void onDecisionTableSelected(final @Observes DecisionTableSelectedEvent event) { super.onDecisionTableSelected(event); if (event.isLockRequired()) { if (!isReadOnly()) { lockManager.acquireLock(); } } } @Override public void makeMenuBar() { this.menus = fileMenuBuilder .addSave(getSaveMenuItem()) .addCopy(versionRecordManager::getCurrentPath, fileNameValidator) .addRename(versionRecordManager::getPathToLatest, fileNameValidator) .addDelete(versionRecordManager::getPathToLatest) .addValidate(() -> onValidate(getActiveDocument())) .addNewTopLevelMenu(getEditMenuItem()) .addNewTopLevelMenu(getViewMenuItem()) .addNewTopLevelMenu(getInsertMenuItem()) .addNewTopLevelMenu(getRadarMenuItem()) .addNewTopLevelMenu(getRegisteredDocumentsMenuItem()) .addNewTopLevelMenu(getVersionManagerMenuItem()) .build(); } @Override protected void enableMenus(final boolean enabled) { super.enableMenus(enabled); getRegisteredDocumentsMenuItem().setEnabled(enabled); } @Override public void getAvailableDocumentPaths(final Callback<List<Path>> callback) { view.showLoading(); graphService.call(new RemoteCallback<List<Path>>() { @Override public void callback(final List<Path> paths) { view.hideBusyIndicator(); callback.callback(paths); } }, new HasBusyIndicatorDefaultErrorCallback(view)).listDecisionTablesInPackage(editorPath); } @Override public void onOpenDocumentsInEditor(final List<Path> selectedDocumentPaths) { if (selectedDocumentPaths == null || selectedDocumentPaths.isEmpty()) { return; } view.showLoading(); loadGraphLatch = new LoadGraphLatch(selectedDocumentPaths.size(), getSelectDecisionTableCommand(selectedDocumentPaths.get(0))); selectedDocumentPaths.stream().forEach((p) -> { final PathPlaceRequest placeRequest = getPathPlaceRequest(p); loadGraphLatch.loadDocument(placeRequest.getPath(), placeRequest); }); } @Override protected void doSave() { if (isReadOnly()) { if (versionRecordManager.isCurrentLatest()) { view.alertReadOnly(); return; } else { versionRecordManager.restoreToCurrentVersion(); return; } } final Set<GuidedDecisionTableView.Presenter> allDecisionTables = new HashSet<>(modeller.getAvailableDecisionTables()); final Set<ObservablePath.OnConcurrentUpdateEvent> concurrentUpdateSessionInfos = new HashSet<>(); allDecisionTables.stream().forEach(dtPresenter -> { final ObservablePath.OnConcurrentUpdateEvent concurrentUpdateSessionInfo = dtPresenter.getConcurrentUpdateSessionInfo(); if (concurrentUpdateSessionInfo != null) { concurrentUpdateSessionInfos.add(concurrentUpdateSessionInfo); } }); if (concurrentUpdateSessionInfo != null) { concurrentUpdateSessionInfos.add(concurrentUpdateSessionInfo); } if (!concurrentUpdateSessionInfos.isEmpty()) { showConcurrentUpdatesPopup(); } else { saveDocumentGraphEntries(); } } void showConcurrentUpdatesPopup() { newConcurrentUpdate(concurrentUpdateSessionInfo.getPath(), concurrentUpdateSessionInfo.getIdentity(), this::saveDocumentGraphEntries, () -> {/*Do nothing*/}, this::reload).show(); } void saveDocumentGraphEntries() { final Set<GuidedDecisionTableView.Presenter> allDecisionTables = new HashSet<>(modeller.getAvailableDecisionTables()); savePopUpPresenter.show(editorPath, (commitMessage) -> { editorView.showSaving(); saveGraphLatch = new SaveGraphLatch(allDecisionTables.size(), commitMessage); allDecisionTables.stream().forEach((dtPresenter) -> { saveGraphLatch.saveDocumentGraphEntry(dtPresenter); saveInProgressEvent.fire(new SaveInProgressEvent(dtPresenter.getLatestPath())); }); }); } @Override protected void initialiseVersionManager(final GuidedDecisionTableView.Presenter dtPresenter) { //Do nothing. We maintain a single VersionRecordManager for the graph itself. } @Override protected void initialiseKieEditorTabs(final GuidedDecisionTableView.Presenter document, final Overview overview, final AsyncPackageDataModelOracle dmo, final Imports imports, final boolean isReadOnly) { kieEditorWrapperView.clear(); kieEditorWrapperView.addMainEditorPage(editorView); kieEditorWrapperView.addOverviewPage(overviewWidget, () -> overviewWidget.refresh(versionRecordManager.getVersion())); kieEditorWrapperView.addSourcePage(sourceWidget); kieEditorWrapperView.addImportsTab(importsWidget); overviewWidget.setContent(content.getOverview(), versionRecordManager.getPathToLatest()); importsWidget.setContent(dmo, imports, isReadOnly); } void initialiseEditorTabsWhenNoDocuments() { getEditMenuItem().setEnabled(false); getViewMenuItem().setEnabled(false); getInsertMenuItem().setEnabled(false); getRadarMenuItem().setEnabled(false); enableMenuItem(false, MenuItems.VALIDATE); kieEditorWrapperView.clear(); kieEditorWrapperView.addMainEditorPage(editorView); kieEditorWrapperView.addOverviewPage(overviewWidget, () -> overviewWidget.refresh(versionRecordManager.getVersion())); overviewWidget.setContent(content.getOverview(), versionRecordManager.getPathToLatest()); } void addFileChangeListeners(final ObservablePath path) { path.onRename(this::onRename); path.onDelete(this::onDelete); path.onConcurrentUpdate((info) -> concurrentUpdateSessionInfo = info); path.onConcurrentRename((info) -> newConcurrentRename(info.getSource(), info.getTarget(), info.getIdentity(), () -> enableMenus(false), this::reload).show()); path.onConcurrentDelete((info) -> newConcurrentDelete(info.getPath(), info.getIdentity(), () -> enableMenus(false), () -> placeManager.closePlace(editorPlaceRequest)).show()); } void onDelete() { scheduleClosure(() -> placeManager.forceClosePlace(editorPlaceRequest)); } void scheduleClosure(final Scheduler.ScheduledCommand command) { Scheduler.get().scheduleDeferred(command); } void onRename() { reload(); changeTitleEvent.fire(new ChangeTitleWidgetEvent(editorPlaceRequest, getTitleText(), editorView.getTitleWidget())); } void reload() { final List<GuidedDecisionTableView.Presenter> documents = new ArrayList<>(this.documents); documents.stream().forEach(this::deregisterDocument); modeller.getView().clear(); modeller.releaseDecisionTables(); loadDocumentGraph(versionRecordManager.getCurrentPath()); } void onRestore(final @Observes RestoreEvent restore) { if (versionRecordManager.getCurrentPath() == null || restore == null || restore.getPath() == null) { return; } if (versionRecordManager.getCurrentPath().equals(restore.getPath())) { initialiseEditor(versionRecordManager.getPathToLatest(), editorPlaceRequest); notification.fire(new NotificationEvent(CommonConstants.INSTANCE.ItemRestored())); } } private boolean isReadOnly() { return !this.access.isEditable(); } void onUpdatedLockStatusEvent(final @Observes UpdatedLockStatusEvent event) { if (editorPath == null) { return; } if (editorPath.equals(event.getFile())) { if (event.isLocked()) { access.setLock(event.isLockedByCurrentUser() ? CURRENT_USER : OTHER_USER); } else { access.setLock(NOBODY); } } } private class LoadGraphLatch { private int dtGraphElementCount; private Command onAllDocumentGraphEntriesLoadedCommand; private ParameterizedCommand<GuidedDecisionTableView.Presenter> onDocumentGraphEntryLoadedCommand; private LoadGraphLatch(final int dtGraphElementCount, final ParameterizedCommand<GuidedDecisionTableView.Presenter> onDocumentGraphEntryLoadedCommand) { this(dtGraphElementCount, onDocumentGraphEntryLoadedCommand, () -> {/*Do nothing*/}); } private LoadGraphLatch(final int dtGraphElementCount, final ParameterizedCommand<GuidedDecisionTableView.Presenter> onDocumentGraphEntryLoadedCommand, final Command onAllDocumentGraphEntriesLoadedCommand) { this.dtGraphElementCount = dtGraphElementCount; this.onDocumentGraphEntryLoadedCommand = onDocumentGraphEntryLoadedCommand; this.onAllDocumentGraphEntriesLoadedCommand = onAllDocumentGraphEntriesLoadedCommand; } private void onDocumentGraphEntryLoaded(final GuidedDecisionTableView.Presenter dtPresenter) { if (onDocumentGraphEntryLoadedCommand != null) { onDocumentGraphEntryLoadedCommand.execute(dtPresenter); } } private void hideLoadingIndicator() { dtGraphElementCount--; if (dtGraphElementCount == 0) { if (onAllDocumentGraphEntriesLoadedCommand != null) { onAllDocumentGraphEntriesLoadedCommand.execute(); } view.hideBusyIndicator(); } } private void loadDocumentGraphEntry(final GuidedDecisionTableEditorGraphModel.GuidedDecisionTableGraphEntry entry) { final PathPlaceRequest placeRequest = getPathPlaceRequest(entry.getPathHead()); final ObservablePath pathHead = placeRequest.getPath(); final Path pathVersion = entry.getPathVersion(); final Double x = entry.getX(); final Double y = entry.getY(); if (isReadOnly()) { placeRequest.addParameter("readOnly", ""); } service.call(getLoadDocumentGraphEntryContentSuccessCallback(pathHead, placeRequest, x, y), getLoadErrorCallback()).loadContent(pathVersion); } private RemoteCallback<GuidedDecisionTableEditorContent> getLoadDocumentGraphEntryContentSuccessCallback(final ObservablePath path, final PlaceRequest placeRequest, final Double x, final Double y) { return (content) -> { //Path is set to null when the Editor is closed (which can happen before async calls complete). if (path == null) { return; } //Add Decision Table to modeller final GuidedDecisionTableView.Presenter dtPresenter = modeller.addDecisionTable(path, placeRequest, content, placeRequest.getParameter("readOnly", null) != null, x, y); registerDocument(dtPresenter); onDocumentGraphEntryLoaded(dtPresenter); hideLoadingIndicator(); }; } private void loadDocument(final ObservablePath path, final PlaceRequest placeRequest) { service.call(getLoadContentSuccessCallback(path, placeRequest), getLoadErrorCallback()).loadContent(path); } private RemoteCallback<GuidedDecisionTableEditorContent> getLoadContentSuccessCallback(final ObservablePath path, final PlaceRequest placeRequest) { return (content) -> { //Path is set to null when the Editor is closed (which can happen before async calls complete). if (path == null) { return; } //Add Decision Table to modeller final GuidedDecisionTableView.Presenter dtPresenter = modeller.addDecisionTable(path, placeRequest, content, placeRequest.getParameter("readOnly", null) != null, null, null); registerDocument(dtPresenter); onDocumentGraphEntryLoaded(dtPresenter); hideLoadingIndicator(); }; } private DefaultErrorCallback getLoadErrorCallback() { final CommandDrivenErrorCallback wrapped = getNoSuchFileExceptionErrorCallback(); final DefaultErrorCallback callback = new DefaultErrorCallback() { @Override public boolean error(final Message message, final Throwable throwable) { hideLoadingIndicator(); return wrapped.error(message, throwable); } }; return callback; } } private class SaveGraphLatch { private final String commitMessage; private int dtGraphElementCount = 0; private SaveGraphLatch(final int dtGraphElementCount, final String commitMessage) { this.dtGraphElementCount = dtGraphElementCount; this.commitMessage = commitMessage; } private void saveDocumentGraph() { dtGraphElementCount--; if (dtGraphElementCount > 0) { return; } final GuidedDecisionTableEditorGraphModel model = buildModelFromEditor(); graphService.call(new RemoteCallback<Path>() { @Override public void callback(final Path path) { editorView.hideBusyIndicator(); versionRecordManager.reloadVersions(path); originalGraphHash = model.hashCode(); concurrentUpdateSessionInfo = null; notificationEvent.fire(new NotificationEvent(CommonConstants.INSTANCE.ItemSavedSuccessfully())); } }, new HasBusyIndicatorDefaultErrorCallback(view)).save(editorPath, model, content.getOverview().getMetadata(), commitMessage); } private void saveDocumentGraphEntry(final GuidedDecisionTableView.Presenter dtPresenter) { final ObservablePath path = dtPresenter.getCurrentPath(); final GuidedDecisionTable52 model = dtPresenter.getModel(); final Metadata metadata = dtPresenter.getOverview().getMetadata(); service.call(getSaveSuccessCallback(dtPresenter, model.hashCode()), getSaveErrorCallback()).save(path, model, metadata, commitMessage); } private RemoteCallback<Path> getSaveSuccessCallback(final GuidedDecisionTableView.Presenter document, final int currentHashCode) { return (path) -> { document.setConcurrentUpdateSessionInfo(null); document.setOriginalHashCode(currentHashCode); saveDocumentGraph(); }; } private DefaultErrorCallback getSaveErrorCallback() { return new HasBusyIndicatorDefaultErrorCallback(view) { @Override public boolean error(final Message message, final Throwable throwable) { saveDocumentGraph(); return super.error(message, throwable); } }; } } }