/* * 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.kie.workbench.common.widgets.metadata.client; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.PostConstruct; import javax.enterprise.event.Event; import javax.enterprise.event.Observes; import javax.inject.Inject; import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.ui.IsWidget; import org.drools.workbench.models.datamodel.imports.Imports; import org.guvnor.common.services.project.context.ProjectContext; import org.guvnor.common.services.shared.metadata.model.Overview; import org.guvnor.structure.repositories.RepositoryRemovedEvent; import org.jboss.errai.bus.client.api.messaging.Message; import org.jboss.errai.common.client.api.RemoteCallback; import org.kie.workbench.common.widgets.client.callbacks.CommandBuilder; 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.menu.FileMenuBuilder; import org.kie.workbench.common.widgets.client.source.ViewDRLSourceWidget; import org.kie.workbench.common.widgets.configresource.client.widget.bound.ImportsWidgetPresenter; import org.kie.workbench.common.widgets.metadata.client.menu.RegisteredDocumentsMenuBuilder; import org.kie.workbench.common.widgets.metadata.client.widget.OverviewWidgetPresenter; import org.uberfire.backend.vfs.ObservablePath; import org.uberfire.backend.vfs.Path; import org.uberfire.client.workbench.events.ChangeTitleWidgetEvent; import org.uberfire.commons.validation.PortablePreconditions; import org.uberfire.ext.editor.commons.client.BaseEditorView; import org.uberfire.ext.editor.commons.client.file.popups.SavePopUpPresenter; import org.uberfire.ext.editor.commons.client.history.VersionRecordManager; 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.client.validation.DefaultFileNameValidator; import org.uberfire.ext.editor.commons.version.events.RestoreEvent; import org.uberfire.ext.widgets.common.client.common.ConcurrentChangePopup; import org.uberfire.mvp.Command; import org.uberfire.mvp.ParameterizedCommand; import org.uberfire.mvp.PlaceRequest; import org.uberfire.workbench.events.NotificationEvent; import org.uberfire.workbench.model.menu.MenuItem; import org.uberfire.workbench.model.menu.Menus; 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; /** * A base for Multi-Document-Interface editors. This base implementation adds default Menus for Save", "Copy", * "Rename", "Delete", "Validate" and "VersionRecordManager" drop-down that can be overriden by subclasses. * {@link KieDocument} documents are first registered and then activated. Registration ensures the document * is configured for optimistic concurrent lock handling. Activation updates the content of the editor to * reflect the active document. * @param <D> Document type */ public abstract class KieMultipleDocumentEditor<D extends KieDocument> implements KieMultipleDocumentEditorPresenter<D> { //Injected protected KieMultipleDocumentEditorWrapperView kieEditorWrapperView; protected OverviewWidgetPresenter overviewWidget; protected ImportsWidgetPresenter importsWidget; protected Event<NotificationEvent> notificationEvent; protected Event<ChangeTitleWidgetEvent> changeTitleEvent; protected ProjectContext workbenchContext; protected SavePopUpPresenter savePopUpPresenter; protected FileMenuBuilder fileMenuBuilder; protected VersionRecordManager versionRecordManager; protected RegisteredDocumentsMenuBuilder registeredDocumentsMenuBuilder; protected DefaultFileNameValidator fileNameValidator; //Constructed protected BaseEditorView editorView; protected ViewDRLSourceWidget sourceWidget = GWT.create(ViewDRLSourceWidget.class); private MenuItem saveMenuItem; private MenuItem versionMenuItem; private MenuItem registeredDocumentsMenuItem; protected Menus menus; private D activeDocument = null; protected final Set<D> documents = new HashSet<>(); //Handler for MayClose requests protected interface MayCloseHandler { boolean mayClose(final Integer originalHashCode, final Integer currentHashCode); } //The default implementation delegates to the HashCode comparison in BaseEditor private final MayCloseHandler DEFAULT_MAY_CLOSE_HANDLER = this::doMayClose; //This implementation always permits closure as something went wrong loading the Editor's content private final MayCloseHandler EXCEPTION_MAY_CLOSE_HANDLER = (originalHashCode, currentHashCode) -> true; private MayCloseHandler mayCloseHandler = DEFAULT_MAY_CLOSE_HANDLER; @SuppressWarnings("unused") KieMultipleDocumentEditor() { //Zero-parameter constructor for CDI proxy } public KieMultipleDocumentEditor(final KieEditorView editorView) { this.editorView = editorView; } @PostConstruct protected void setupMenuBar() { makeMenuBar(); kieEditorWrapperView.init(this); } @Inject protected void setKieEditorWrapperView(final @KieMultipleDocumentEditorQualifier KieMultipleDocumentEditorWrapperView kieEditorWrapperView) { this.kieEditorWrapperView = kieEditorWrapperView; this.kieEditorWrapperView.setPresenter(this); } @Inject protected void setOverviewWidget(final OverviewWidgetPresenter overviewWidget) { this.overviewWidget = overviewWidget; } @Inject protected void setSavePopUpPresenter(final SavePopUpPresenter savePopUpPresenter) { this.savePopUpPresenter = savePopUpPresenter; } @Inject protected void setImportsWidget(final ImportsWidgetPresenter importsWidget) { this.importsWidget = importsWidget; } @Inject protected void setNotificationEvent(final Event<NotificationEvent> notificationEvent) { this.notificationEvent = notificationEvent; } @Inject protected void setChangeTitleEvent(final Event<ChangeTitleWidgetEvent> changeTitleEvent) { this.changeTitleEvent = changeTitleEvent; } @Inject protected void setWorkbenchContext(final ProjectContext workbenchContext) { this.workbenchContext = workbenchContext; } @Inject protected void setVersionRecordManager(final VersionRecordManager versionRecordManager) { this.versionRecordManager = versionRecordManager; this.versionRecordManager.setShowMoreCommand(() -> { kieEditorWrapperView.selectOverviewTab(); overviewWidget.showVersionsTab(); }); } @Inject protected void setFileMenuBuilder(final FileMenuBuilder fileMenuBuilder) { this.fileMenuBuilder = fileMenuBuilder; } @Inject protected void setRegisteredDocumentsMenuBuilder(final RegisteredDocumentsMenuBuilder registeredDocumentsMenuBuilder) { this.registeredDocumentsMenuBuilder = registeredDocumentsMenuBuilder; } @Inject protected void setFileNameValidator(final DefaultFileNameValidator fileNameValidator) { this.fileNameValidator = fileNameValidator; } @Override public void registerDocument(final D document) { PortablePreconditions.checkNotNull("document", document); if (documents.contains(document)) { return; } documents.add(document); registeredDocumentsMenuBuilder.registerDocument(document); //Setup concurrent modification handlers final ObservablePath path = document.getLatestPath(); path.onRename(() -> refresh(document)); path.onConcurrentRename((info) -> doConcurrentRename(document, info)); path.onDelete(() -> { enableMenus(false); removeDocument(document); deregisterDocument(document); }); path.onConcurrentDelete((info) -> { doConcurrentDelete(document, info); }); path.onConcurrentUpdate((eventInfo) -> document.setConcurrentUpdateSessionInfo(eventInfo)); } //Package protected to allow overriding for Unit Tests void doConcurrentRename(final D document, final ObservablePath.OnConcurrentRenameEvent info) { newConcurrentRename(info.getSource(), info.getTarget(), info.getIdentity(), getConcurrentRenameOnIgnoreCommand(), getConcurrentRenameOnReopenCommand(document)).show(); } //Package protected to allow overriding for Unit Tests Command getConcurrentRenameOnIgnoreCommand() { return () -> enableMenus(false); } //Package protected to allow overriding for Unit Tests Command getConcurrentRenameOnReopenCommand(final D document) { return () -> { document.setConcurrentUpdateSessionInfo(null); refresh(document); }; } //Package protected to allow overriding for Unit Tests void doConcurrentDelete(final D document, final ObservablePath.OnConcurrentDelete info) { newConcurrentDelete(info.getPath(), info.getIdentity(), getConcurrentDeleteOnIgnoreCommand(), getConcurrentDeleteOnClose(document)).show(); } //Package protected to allow overriding for Unit Tests Command getConcurrentDeleteOnIgnoreCommand() { return () -> enableMenus(false); } //Package protected to allow overriding for Unit Tests Command getConcurrentDeleteOnClose(final D document) { return () -> { enableMenus(false); removeDocument(document); deregisterDocument(document); }; } @Override public void deregisterDocument(final D document) { PortablePreconditions.checkNotNull("document", document); if (!documents.contains(document)) { return; } registeredDocumentsMenuBuilder.deregisterDocument(document); document.getLatestPath().dispose(); documents.remove(document); } private void refresh(final D document) { final String documentTitle = getDocumentTitle(document); editorView.refreshTitle(documentTitle); editorView.showBusyIndicator(CommonConstants.INSTANCE.Loading()); refreshDocument(document); final PlaceRequest placeRequest = document.getPlaceRequest(); changeTitleEvent.fire(new ChangeTitleWidgetEvent(placeRequest, documentTitle, getTitleWidget(document))); } @Override public void activateDocument(final D document, final Overview overview, final AsyncPackageDataModelOracle dmo, final Imports imports, final boolean isReadOnly) { PortablePreconditions.checkNotNull("document", document); PortablePreconditions.checkNotNull("overview", overview); PortablePreconditions.checkNotNull("dmo", document); PortablePreconditions.checkNotNull("imports", imports); if (!documents.contains(document)) { throw new IllegalArgumentException("Document has not been registered."); } activeDocument = document; registeredDocumentsMenuBuilder.activateDocument(document); initialiseVersionManager(document); initialiseKieEditorTabs(document, overview, dmo, imports, isReadOnly); } @Override public D getActiveDocument() { return activeDocument; } protected void initialiseVersionManager(final D document) { final String version = document.getVersion(); final ObservablePath path = document.getLatestPath(); versionRecordManager.init(version, path, (versionRecord) -> { versionRecordManager.setVersion(versionRecord.id()); document.setVersion(versionRecord.id()); document.setCurrentPath(versionRecordManager.getCurrentPath()); document.setReadOnly(!versionRecordManager.isLatest(versionRecord)); refreshDocument(document); }); } protected void initialiseKieEditorTabs(final D 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(overview, document.getLatestPath()); importsWidget.setContent(dmo, imports, isReadOnly); } @Override public IsWidget getWidget() { return kieEditorWrapperView.asWidget(); } @Override public IsWidget getTitleWidget(final D document) { PortablePreconditions.checkNotNull("document", document); editorView.refreshTitle(getDocumentTitle(document)); return editorView.getTitleWidget(); } @Override public Menus getMenus() { return this.menus; } @Override public void onClose() { final List<D> documents = new ArrayList<>(this.documents); documents.stream().forEach(this::deregisterDocument); this.versionRecordManager.clear(); this.activeDocument = null; } @Override public void onSourceTabSelected() { onSourceTabSelected(getActiveDocument()); } @Override public void updateSource(final String source) { sourceWidget.setContent(source); } @Override public void onEditTabSelected() { //Nothing to do } @Override public void onEditTabUnselected() { //Nothing to do } @Override public void onOverviewSelected() { //Nothing to do } @Override public boolean mayClose(final Integer originalHashCode, final Integer currentHashCode) { return mayCloseHandler.mayClose(originalHashCode, currentHashCode); } protected void setMayCloseHandler(final MayCloseHandler mayCloseHandler) { this.mayCloseHandler = mayCloseHandler; } protected boolean doMayClose(final Integer originalHashCode, final Integer currentHashCode) { if (this.isDirty(originalHashCode, currentHashCode) || overviewWidget.isDirty()) { return this.editorView.confirmClose(); } else { return true; } } protected boolean isDirty(final Integer originalHashCode, final Integer currentHashCode) { if (originalHashCode == null) { return currentHashCode != null; } else { return !originalHashCode.equals(currentHashCode); } } /** * Construct the default Menus, consisting of "Save", "Copy", "Rename", "Delete", * "Validate" and "VersionRecordManager" drop-down. Subclasses can override this * to customize their Menus. */ @Override public void makeMenuBar() { this.fileMenuBuilder.setLockSyncMenuStateHelper(new KieMultipleDocumentEditorLockSyncHelper(this)); this.menus = fileMenuBuilder .addSave(getSaveMenuItem()) .addCopy(() -> getActiveDocument().getCurrentPath(), fileNameValidator) .addRename(() -> getActiveDocument().getLatestPath(), fileNameValidator) .addDelete(() -> getActiveDocument().getLatestPath()) .addValidate(() -> onValidate(getActiveDocument())) .addNewTopLevelMenu(getRegisteredDocumentsMenuItem()) .addNewTopLevelMenu(getVersionManagerMenuItem()) .build(); } /** * Get the MenuItem that should be used for "Save". * @return */ protected MenuItem getSaveMenuItem() { if (saveMenuItem == null) { saveMenuItem = versionRecordManager.newSaveMenuItem(this::doSave); } return saveMenuItem; } /** * Get the MenuItem that should be used for listing "(Registered) documents". * @return */ protected MenuItem getRegisteredDocumentsMenuItem() { if (registeredDocumentsMenuItem == null) { registeredDocumentsMenuItem = registeredDocumentsMenuBuilder.build(); registeredDocumentsMenuBuilder.setOpenDocumentCommand(this::openDocumentInEditor); } return registeredDocumentsMenuItem; } /** * Get the MenuItem that should be used for "VersionRecordManager" drop-down. * @return */ protected MenuItem getVersionManagerMenuItem() { if (versionMenuItem == null) { versionMenuItem = versionRecordManager.buildMenu(); } return versionMenuItem; } /** * Called by the "Save" MenuItem to save or restore the active document. If the active document * is read-only a check is made whether the active document is an older version; in which case * the active document is restored. If the active document is read-only and the latest version * the User is notified that the document is read-only and the save aborted. If the document * is not read-only a check is made for concurrent updates before persisting. */ protected void doSave() { final D document = getActiveDocument(); if (document == null) { return; } final boolean isReadOnly = document.isReadOnly(); if (isReadOnly) { if (versionRecordManager.isCurrentLatest()) { editorView.alertReadOnly(); } else { versionRecordManager.restoreToCurrentVersion(); } return; } doSaveCheckForAndHandleConcurrentUpdate(document); } /** * Checks whether a document has experienced a concurrent update by another user. If a concurrent update * is detected a {@link ConcurrentChangePopup} is shown allowing the user to choose whether to abort the * save, force the save or refresh the view with the latest version. If no concurrent update is detected * the document is persisted. * @param document */ protected void doSaveCheckForAndHandleConcurrentUpdate(final D document) { final ObservablePath.OnConcurrentUpdateEvent concurrentUpdateSessionInfo = document.getConcurrentUpdateSessionInfo(); if (concurrentUpdateSessionInfo != null) { showConcurrentUpdatePopup(document); } else { doSave(document); } } void showConcurrentUpdatePopup(final D document) { final ObservablePath.OnConcurrentUpdateEvent concurrentUpdateSessionInfo = document.getConcurrentUpdateSessionInfo(); newConcurrentUpdate(concurrentUpdateSessionInfo.getPath(), concurrentUpdateSessionInfo.getIdentity(), () -> doSave(document), () -> {/*nothing*/}, () -> { document.setConcurrentUpdateSessionInfo(null); refresh(document); }).show(); } //Package protected to allow overriding for Unit Tests void doSave(final D document) { savePopUpPresenter.show(document.getCurrentPath(), getSaveCommand(document)); } //Package protected to allow overriding for Unit Tests ParameterizedCommand<String> getSaveCommand(final D document) { return (commitMessage) -> { editorView.showSaving(); onSave(document, commitMessage); document.setConcurrentUpdateSessionInfo(null); }; } void onRestore(final @Observes RestoreEvent event) { if (event == null || event.getPath() == null) { return; } if (versionRecordManager.getCurrentPath() == null) { return; } if (versionRecordManager.getCurrentPath().equals(event.getPath())) { activeDocument.setVersion(null); activeDocument.setLatestPath(versionRecordManager.getPathToLatest()); activeDocument.setCurrentPath(versionRecordManager.getPathToLatest()); initialiseVersionManager(activeDocument); activeDocument.setReadOnly(false); refreshDocument(activeDocument); notificationEvent.fire(new NotificationEvent(CommonConstants.INSTANCE.ItemRestored())); } } void onRepositoryRemoved(final @Observes RepositoryRemovedEvent event) { if (event.getRepository() == null) { return; } if (workbenchContext == null) { return; } if (workbenchContext.getActiveRepository() == null) { return; } if (workbenchContext.getActiveRepository().equals(event.getRepository())) { enableMenus(false); } } /** * Enable/disable all menus associated with the MDI container, consisting of "Save", "Copy", * "Rename", "Delete", "Validate" and "VersionRecordManager" drop-down. Subclasses can override * this to customize their Menus. * @param enabled */ protected void enableMenus(final boolean enabled) { getSaveMenuItem().setEnabled(enabled); getVersionManagerMenuItem().setEnabled(enabled); enableMenuItem(enabled, MenuItems.COPY); enableMenuItem(enabled, MenuItems.RENAME); enableMenuItem(enabled, MenuItems.DELETE); enableMenuItem(enabled, MenuItems.VALIDATE); } /** * Enable/disable a single menu associated with the MDI container. * @param enabled */ protected void enableMenuItem(final boolean enabled, final MenuItems menuItem) { if (menus.getItemsMap().containsKey(menuItem)) { menus.getItemsMap().get(menuItem).setEnabled(enabled); } } protected void openDocumentInEditor() { getAvailableDocumentPaths((allPaths) -> { for (D document : documents) { allPaths.remove(document.getLatestPath()); } if (allPaths.isEmpty()) { kieEditorWrapperView.showNoAdditionalDocuments(); } else { kieEditorWrapperView.showAdditionalDocuments(allPaths); } }); } /** * Default callback for when loading a document fails. * @return */ protected CommandDrivenErrorCallback getNoSuchFileExceptionErrorCallback() { return new CommandDrivenErrorCallback(editorView, new CommandBuilder() .addNoSuchFileException(editorView, kieEditorWrapperView.getMultiPage(), menus) .addFileSystemNotFoundException(editorView, kieEditorWrapperView.getMultiPage(), menus) .build() ) { @Override public boolean error(final Message message, final Throwable throwable) { mayCloseHandler = EXCEPTION_MAY_CLOSE_HANDLER; return super.error(message, throwable); } }; } /** * Default callback for when retrieval of a document's source fails. * @return */ protected CommandDrivenErrorCallback getCouldNotGenerateSourceErrorCallback() { return new CommandDrivenErrorCallback(editorView, new CommandBuilder() .addSourceCodeGenerationFailedException(editorView, sourceWidget) .build() ); } /** * Default callback for when a document has been saved. This should be used by implementations * of {@link #onSave(KieDocument, String)} to ensure the "isDirty" mechanism is correctly updated. * @param document * @param currentHashCode * @return */ protected RemoteCallback<Path> getSaveSuccessCallback(final D document, final int currentHashCode) { return (path) -> { editorView.hideBusyIndicator(); versionRecordManager.reloadVersions(path); notificationEvent.fire(new NotificationEvent(CommonConstants.INSTANCE.ItemSavedSuccessfully())); document.setOriginalHashCode(currentHashCode); }; } }