/* * Copyright 2017 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.stunner.project.client.editor; import java.util.Collection; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.PostConstruct; import javax.enterprise.event.Event; import javax.enterprise.event.Observes; import javax.inject.Inject; import com.google.gwt.logging.client.LogConfiguration; import com.google.gwt.user.client.ui.IsWidget; import org.kie.workbench.common.stunner.client.widgets.presenters.session.SessionPresenter; import org.kie.workbench.common.stunner.client.widgets.presenters.session.SessionPresenterFactory; import org.kie.workbench.common.stunner.core.client.api.SessionManager; import org.kie.workbench.common.stunner.core.client.canvas.CanvasHandler; import org.kie.workbench.common.stunner.core.client.service.ClientRuntimeError; import org.kie.workbench.common.stunner.core.client.service.ServiceCallback; import org.kie.workbench.common.stunner.core.client.session.ClientFullSession; import org.kie.workbench.common.stunner.core.client.session.ClientSession; import org.kie.workbench.common.stunner.core.client.session.command.ClientSessionCommand; import org.kie.workbench.common.stunner.core.client.session.command.impl.ClearSessionCommand; import org.kie.workbench.common.stunner.core.client.session.command.impl.ClearStatesSessionCommand; import org.kie.workbench.common.stunner.core.client.session.command.impl.DeleteSelectionSessionCommand; import org.kie.workbench.common.stunner.core.client.session.command.impl.RedoSessionCommand; import org.kie.workbench.common.stunner.core.client.session.command.impl.RefreshSessionCommand; import org.kie.workbench.common.stunner.core.client.session.command.impl.SessionCommandFactory; import org.kie.workbench.common.stunner.core.client.session.command.impl.SwitchGridSessionCommand; import org.kie.workbench.common.stunner.core.client.session.command.impl.UndoSessionCommand; import org.kie.workbench.common.stunner.core.client.session.command.impl.ValidateSessionCommand; import org.kie.workbench.common.stunner.core.client.session.command.impl.VisitGraphSessionCommand; import org.kie.workbench.common.stunner.core.client.session.event.OnSessionErrorEvent; import org.kie.workbench.common.stunner.core.client.session.impl.AbstractClientFullSession; import org.kie.workbench.common.stunner.core.client.session.impl.AbstractClientReadOnlySession; import org.kie.workbench.common.stunner.core.client.util.ClientSessionUtils; import org.kie.workbench.common.stunner.core.diagram.Diagram; import org.kie.workbench.common.stunner.core.rule.RuleViolation; import org.kie.workbench.common.stunner.core.validation.DiagramElementViolation; import org.kie.workbench.common.stunner.core.validation.Violation; import org.kie.workbench.common.stunner.core.validation.impl.ValidationUtils; import org.kie.workbench.common.stunner.project.client.service.ClientProjectDiagramService; import org.kie.workbench.common.stunner.project.diagram.ProjectDiagram; import org.kie.workbench.common.widgets.metadata.client.KieEditor; import org.kie.workbench.common.widgets.metadata.client.KieEditorView; import org.uberfire.backend.vfs.ObservablePath; import org.uberfire.client.mvp.PlaceManager; import org.uberfire.client.mvp.UberView; import org.uberfire.client.workbench.events.ChangeTitleWidgetEvent; import org.uberfire.client.workbench.type.ClientResourceType; import org.uberfire.client.workbench.widgets.common.ErrorPopupPresenter; import org.uberfire.ext.editor.commons.client.file.popups.SavePopUpPresenter; import org.uberfire.ext.widgets.common.client.common.popups.YesNoCancelPopup; import org.uberfire.mvp.Command; import org.uberfire.mvp.PlaceRequest; import org.uberfire.workbench.model.menu.MenuItem; import org.uberfire.workbench.model.menu.Menus; import static java.util.logging.Level.FINE; // TODO: i18n. public abstract class AbstractProjectDiagramEditor<R extends ClientResourceType> extends KieEditor { private static Logger LOGGER = Logger.getLogger(AbstractProjectDiagramEditor.class.getName()); public interface View extends UberView<AbstractProjectDiagramEditor>, KieEditorView, IsWidget { void setWidget(IsWidget widget); } private final PlaceManager placeManager; private final ErrorPopupPresenter errorPopupPresenter; private final Event<ChangeTitleWidgetEvent> changeTitleNotificationEvent; private final R resourceType; private final ClientProjectDiagramService projectDiagramServices; private final SessionManager sessionManager; private final SessionPresenterFactory<Diagram, AbstractClientReadOnlySession, AbstractClientFullSession> sessionPresenterFactory; private final ClientSessionUtils sessionUtils; private final ProjectDiagramEditorMenuItemsBuilder menuItemsBuilder; private final ClearStatesSessionCommand sessionClearStatesCommand; private final VisitGraphSessionCommand sessionVisitGraphCommand; private final SwitchGridSessionCommand sessionSwitchGridCommand; private final ClearSessionCommand sessionClearCommand; private final DeleteSelectionSessionCommand sessionDeleteSelectionCommand; private final UndoSessionCommand sessionUndoCommand; private final RedoSessionCommand sessionRedoCommand; private final ValidateSessionCommand sessionValidateCommand; private final RefreshSessionCommand sessionRefreshCommand; protected SessionPresenter<AbstractClientFullSession, ?, Diagram> presenter; private String title = "Project Diagram Editor"; @Inject public AbstractProjectDiagramEditor(final View view, final PlaceManager placeManager, final ErrorPopupPresenter errorPopupPresenter, final Event<ChangeTitleWidgetEvent> changeTitleNotificationEvent, final SavePopUpPresenter savePopUpPresenter, final R resourceType, final ClientProjectDiagramService projectDiagramServices, final SessionManager sessionManager, final SessionPresenterFactory<Diagram, AbstractClientReadOnlySession, AbstractClientFullSession> sessionPresenterFactory, final ClientSessionUtils sessionUtils, final SessionCommandFactory sessionCommandFactory, final ProjectDiagramEditorMenuItemsBuilder menuItemsBuilder) { super(view); this.placeManager = placeManager; this.errorPopupPresenter = errorPopupPresenter; this.changeTitleNotificationEvent = changeTitleNotificationEvent; this.savePopUpPresenter = savePopUpPresenter; this.resourceType = resourceType; this.projectDiagramServices = projectDiagramServices; this.sessionManager = sessionManager; this.sessionPresenterFactory = sessionPresenterFactory; this.sessionUtils = sessionUtils; this.menuItemsBuilder = menuItemsBuilder; this.sessionClearStatesCommand = sessionCommandFactory.newClearStatesCommand(); this.sessionVisitGraphCommand = sessionCommandFactory.newVisitGraphCommand(); this.sessionSwitchGridCommand = sessionCommandFactory.newSwitchGridCommand(); this.sessionClearCommand = sessionCommandFactory.newClearCommand(); this.sessionDeleteSelectionCommand = sessionCommandFactory.newDeleteSelectedElementsCommand(); this.sessionUndoCommand = sessionCommandFactory.newUndoCommand(); this.sessionRedoCommand = sessionCommandFactory.newRedoCommand(); this.sessionValidateCommand = sessionCommandFactory.newValidateCommand(); this.sessionRefreshCommand = sessionCommandFactory.newRefreshSessionCommand(); } protected abstract int getCanvasWidth(); protected abstract int getCanvasHeight(); @PostConstruct @SuppressWarnings("unchecked") public void init() { getView().init(this); } protected void doStartUp(final ObservablePath path, final PlaceRequest place) { init(path, place, resourceType); } @Override protected void loadContent() { projectDiagramServices.getByPath(versionRecordManager.getCurrentPath(), new ServiceCallback<ProjectDiagram>() { @Override public void onSuccess(final ProjectDiagram item) { open(item); } @Override public void onError(final ClientRuntimeError error) { showError(error); } }); } protected void open(final ProjectDiagram diagram) { showLoadingViews(); final AbstractClientFullSession session = newSession(diagram); presenter = sessionPresenterFactory.newPresenterEditor(); getView().setWidget(presenter.getView()); presenter .withToolbar(false) .withPalette(true) .displayNotifications(type -> true) .open(diagram, session, new SessionPresenter.SessionPresenterCallback<AbstractClientFullSession, Diagram>() { @Override public void afterSessionOpened() { } @Override public void afterCanvasInitialized() { } @Override public void onSuccess() { bindCommands(); updateTitle(diagram.getMetadata().getTitle()); hideLoadingViews(); } @Override public void onError(final ClientRuntimeError error) { showError(error); } }); } private AbstractClientFullSession newSession(final Diagram diagram) { return (AbstractClientFullSession) sessionManager.getSessionFactory(diagram, ClientFullSession.class).newSession(); } @Override protected Command onValidate() { return () -> validate(() -> { onValidationSuccess(); hideLoadingViews(); }); } /** * This method is called just once clicking on save. * Before starting the save process, let's perform a diagram validation * to check all it's valid. * It's allowed to continue with the save process event if warnings found, * but cannot save if any error is present in order to * guarantee the diagram's consistency. */ @Override protected void save() { final Command continueSaveOnceValid = () -> super.save(); sessionValidateCommand .execute(new ClientSessionCommand.Callback<Collection<DiagramElementViolation<RuleViolation>>>() { @Override public void onSuccess() { continueSaveOnceValid.execute(); } @Override public void onError(final Collection<DiagramElementViolation<RuleViolation>> violations) { final Violation.Type maxSeverity = ValidationUtils.getMaxSeverity(violations); if (maxSeverity.equals(Violation.Type.ERROR)) { onValidationFailed(violations); } else { // Allow saving when only warnings founds. continueSaveOnceValid.execute(); } } }); } /** * Considering the diagram valid at this point , * it delegates the save operation to the diagram services bean. * @param commitMessage The commit's message. */ @Override @SuppressWarnings("unchecked") protected void save(final String commitMessage) { super.save(commitMessage); showLoadingViews(); // Obtain diagram's image data before saving. final String thumbData = sessionUtils.canvasToImageData(getSession()); // Update diagram's image data as thumbnail. final CanvasHandler canvasHandler = getSession().getCanvasHandler(); final Diagram diagram = canvasHandler.getDiagram(); diagram.getMetadata().setThumbData(thumbData); // Perform update operation remote call. projectDiagramServices.saveOrUpdate(versionRecordManager.getCurrentPath(), getDiagram(), metadata, commitMessage, new ServiceCallback<ProjectDiagram>() { @Override public void onSuccess(final ProjectDiagram item) { getSaveSuccessCallback(item.hashCode()); onSaveSuccess(); hideLoadingViews(); } @Override public void onError(final ClientRuntimeError error) { onSaveError(error); } }); } @Override protected void makeMenuBar() { // TODO: fix - menu items not getting disabled/enabled? final MenuItem clearItem = menuItemsBuilder.newClearItem(AbstractProjectDiagramEditor.this::menu_clear); sessionClearCommand.listen(() -> clearItem.setEnabled(sessionClearCommand.isEnabled())); final MenuItem clearStatesItem = menuItemsBuilder.newClearSelectionItem(AbstractProjectDiagramEditor.this::menu_clearStates); sessionClearStatesCommand.listen(() -> clearStatesItem.setEnabled(sessionClearStatesCommand.isEnabled())); final MenuItem visitGraphItem = menuItemsBuilder.newVisitGraphItem(AbstractProjectDiagramEditor.this::menu_visitGraph); sessionVisitGraphCommand.listen(() -> visitGraphItem.setEnabled(sessionVisitGraphCommand.isEnabled())); final MenuItem switchGridItem = menuItemsBuilder.newSwitchGridItem(AbstractProjectDiagramEditor.this::menu_switchGrid); sessionSwitchGridCommand.listen(() -> switchGridItem.setEnabled(sessionSwitchGridCommand.isEnabled())); final MenuItem deleteSelectionItem = menuItemsBuilder.newDeleteSelectionItem(AbstractProjectDiagramEditor.this::menu_deleteSelected); sessionDeleteSelectionCommand.listen(() -> deleteSelectionItem.setEnabled(sessionDeleteSelectionCommand.isEnabled())); final MenuItem undoItem = menuItemsBuilder.newUndoItem(AbstractProjectDiagramEditor.this::menu_undo); sessionUndoCommand.listen(() -> undoItem.setEnabled(sessionUndoCommand.isEnabled())); final MenuItem redoItem = menuItemsBuilder.newRedoItem(AbstractProjectDiagramEditor.this::menu_redo); sessionRedoCommand.listen(() -> redoItem.setEnabled(sessionRedoCommand.isEnabled())); final MenuItem validateItem = menuItemsBuilder.newValidateItem(AbstractProjectDiagramEditor.this::menu_validate); sessionValidateCommand.listen(() -> validateItem.setEnabled(sessionValidateCommand.isEnabled())); final MenuItem refreshItem = menuItemsBuilder.newRefreshItem(AbstractProjectDiagramEditor.this::menu_refresh); sessionRefreshCommand.listen(() -> refreshItem.setEnabled(sessionRefreshCommand.isEnabled())); // Build the menu. menuBuilder // Specific Stunner toolbar items. .addNewTopLevelMenu(clearItem) .addNewTopLevelMenu(clearStatesItem) .addNewTopLevelMenu(visitGraphItem) .addNewTopLevelMenu(switchGridItem) .addNewTopLevelMenu(deleteSelectionItem) .addNewTopLevelMenu(undoItem) .addNewTopLevelMenu(redoItem) .addNewTopLevelMenu(validateItem) .addNewTopLevelMenu(refreshItem); if (menuItemsBuilder.isDevItemsEnabled()) { menuBuilder.addNewTopLevelMenu(menuItemsBuilder.newDevItems()); } menus = menuBuilder // Project editor menus. .addSave(versionRecordManager.newSaveMenuItem(() -> onSave())) .addCopy(versionRecordManager.getCurrentPath(), fileNameValidator) .addRename(versionRecordManager.getPathToLatest(), fileNameValidator) .addDelete(versionRecordManager.getPathToLatest()) .addNewTopLevelMenu(versionRecordManager.buildMenu()) // Build the menu. .build(); } private void validate(final Command callback) { showLoadingViews(); sessionValidateCommand.execute(new ClientSessionCommand.Callback<Collection<DiagramElementViolation<RuleViolation>>>() { @Override public void onSuccess() { callback.execute(); } @Override public void onError(final Collection<DiagramElementViolation<RuleViolation>> violations) { onValidationFailed(violations); } }); } private void menu_clear() { sessionClearCommand.execute(); } private void menu_clearStates() { sessionClearStatesCommand.execute(); } private void menu_visitGraph() { sessionVisitGraphCommand.execute(); } private void menu_switchGrid() { sessionSwitchGridCommand.execute(); } private void menu_deleteSelected() { sessionDeleteSelectionCommand.execute(); } private void menu_undo() { sessionUndoCommand.execute(); } private void menu_redo() { sessionRedoCommand.execute(); } private void menu_refresh() { showLoadingViews(); sessionRefreshCommand.execute(new ClientSessionCommand.Callback<ClientRuntimeError>() { @Override public void onSuccess() { log(FINE, "Diagram refresh successful."); hideLoadingViews(); } @Override public void onError(final ClientRuntimeError error) { showError(error); } }); } private void menu_validate() { this.validate(() -> hideLoadingViews()); } protected void doOpen() { if (null != getSession()) { sessionManager.resume(getSession()); } } protected void showLoadingViews() { getView().showLoading(); } protected void hideLoadingViews() { getView().hideBusyIndicator(); } protected void doClose() { destroySession(); } protected void doFocus() { log(FINE, "Focusing Stunner Project Diagram Editor..."); if (null != getSession() && !isSameSession(sessionManager.getCurrentSession())) { sessionManager.open(getSession()); } else if (null != getSession()) { log(FINE, "Session already active, no action."); } } protected void doLostFocus() { } void onSessionErrorEvent(final @Observes OnSessionErrorEvent errorEvent) { if (isSameSession(errorEvent.getSession())) { executeWithConfirm("An error happened [" + errorEvent.getError() + "]. Do you want" + "to refresh the diagram (Last changes can be lost)? ", this::menu_refresh); } } private boolean isSameSession(final ClientSession other) { return null != other && null != getSession() && other.equals(getSession()); } public String getTitleText() { return title; } protected Menus getMenus() { if (menus == null) { makeMenuBar(); } return menus; } protected boolean _onMayClose() { return super.mayClose(getCurrentDiagramHash()); } void bindCommands() { this.sessionClearStatesCommand.bind(getSession()); this.sessionVisitGraphCommand.bind(getSession()); this.sessionSwitchGridCommand.bind(getSession()); this.sessionClearCommand.bind(getSession()); this.sessionDeleteSelectionCommand.bind(getSession()); this.sessionUndoCommand.bind(getSession()); this.sessionRedoCommand.bind(getSession()); this.sessionValidateCommand.bind(getSession()); this.sessionRefreshCommand.bind(getSession()); } void unbindCommands() { this.sessionClearStatesCommand.unbind(); this.sessionVisitGraphCommand.unbind(); this.sessionSwitchGridCommand.unbind(); this.sessionClearCommand.unbind(); this.sessionDeleteSelectionCommand.unbind(); this.sessionUndoCommand.unbind(); this.sessionRedoCommand.unbind(); this.sessionValidateCommand.unbind(); this.sessionRefreshCommand.unbind(); } private void pauseSession() { sessionManager.pause(); } private void destroySession() { unbindCommands(); presenter.clear(); presenter.destroy(); } private void updateTitle(final String title) { // Change editor's title. this.title = title; changeTitleNotificationEvent.fire(new ChangeTitleWidgetEvent(this.place, this.title)); } private AbstractClientFullSession getSession() { return null != presenter ? presenter.getInstance() : null; } protected int getCurrentDiagramHash() { if (getDiagram() == null) { return 0; } return getDiagram().hashCode(); } protected CanvasHandler getCanvasHandler() { return null != sessionManager.getCurrentSession() ? sessionManager.getCurrentSession().getCanvasHandler() : null; } protected ProjectDiagram getDiagram() { return null != getCanvasHandler() ? (ProjectDiagram) getCanvasHandler().getDiagram() : null; } private void executeWithConfirm(final String message, final Command command) { final Command yesCommand = command::execute; final Command noCommand = () -> { }; final YesNoCancelPopup popup = YesNoCancelPopup.newYesNoCancelPopup(message, null, yesCommand, noCommand, noCommand); popup.show(); } protected View getView() { return (View) baseView; } private void onSaveSuccess() { final String message = "Diagram saved successfully."; log(Level.INFO, message); presenter.getView().showMessage(message); } private void onSaveError(final ClientRuntimeError error) { showError(error.toString()); } private void onValidationSuccess() { log(Level.INFO, "Validation SUCCESS."); } private void onValidationFailed(final Collection<DiagramElementViolation<RuleViolation>> violations) { log(Level.WARNING, "Validation FAILED [violations=" + violations.toString() + "]"); hideLoadingViews(); } private void showError(final ClientRuntimeError error) { showError(error.toString()); } private void showError(final String message) { log(Level.SEVERE, message); errorPopupPresenter.showMessage(message); hideLoadingViews(); } private void log(final Level level, final String message) { if (LogConfiguration.loggingIsEnabled()) { LOGGER.log(level, message); } } }