blob: f300f1e07d776568dfe3116676e10c8e202f1144 [file] [log] [blame]
/*********************************************************************
* Copyright (c) 2011, 2019 SAP SE
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* Contributors:
* Bug 336488 - DiagramEditor API
* mwenz - Bug 372753 - save shouldn't (necessarily) flush the command stack
* mwenz - Bug 376008 - Iterating through navigation history causes exceptions
* mwenz - Bug 393074 - Save Editor Progress Monitor Argument
* pjpaulin - Bug 352120 - Now uses IDiagramContainerUI interface
* mwenz/Rob Cernich - Bug 391046 - Deadlock while saving prior to refactoring operation
* mwenz - Bug 437933 - NullPointerException in DefaultPersistencyBehavior.isDirty()
* mwenz - Bug 441676 - Read-only attribute is not respected in Graphiti
* mwenz - Bug 473087 - ClassCastException in DefaultPersistencyBehavior.loadDiagram
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipse.graphiti.ui.editor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.emf.common.command.BasicCommandStack;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CommandStack;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.WrappedException;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.transaction.RecordingCommand;
import org.eclipse.emf.transaction.Transaction;
import org.eclipse.emf.transaction.TransactionalEditingDomain;
import org.eclipse.emf.transaction.impl.InternalTransactionalEditingDomain;
import org.eclipse.graphiti.dt.IDiagramTypeProvider;
import org.eclipse.graphiti.internal.IDiagramVersion;
import org.eclipse.graphiti.mm.pictograms.Diagram;
import org.eclipse.graphiti.mm.pictograms.PictogramsPackage;
import org.eclipse.graphiti.ui.internal.GraphitiUIPlugin;
import org.eclipse.graphiti.ui.internal.Messages;
import org.eclipse.graphiti.ui.internal.T;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.operation.IThreadListener;
import org.eclipse.jface.operation.ModalContext;
import org.eclipse.swt.widgets.Display;
/**
* The default implementation for the {@link DiagramBehavior} behavior extension
* that controls the persistence behavior of the Graphiti diagram Editor.
* Clients may subclass to change the behavior; use
* {@link DiagramBehavior#createPersistencyBehavior()} to return the instance
* that shall be used.<br>
* Note that there is always a 1:1 relation with a {@link DiagramBehavior}.
*
* @since 0.9
*/
public class DefaultPersistencyBehavior {
/**
* The associated {@link DiagramBehavior}
*
* @since 0.10
*/
protected final DiagramBehavior diagramBehavior;
/**
* Used to store the command that was executed before the editor was saved.
* By comparing with the top of the current undo stack this point in the
* command stack indicates if the editor is dirty.
*/
protected Command savedCommand = null;
/**
* Creates a new instance of {@link DefaultPersistencyBehavior} that is
* associated with the given {@link DiagramBehavior}.
*
* @param diagramEditor
* the associated {@link DiagramBehavior}
* @since 0.10
*/
public DefaultPersistencyBehavior(DiagramBehavior diagramBehavior) {
this.diagramBehavior = diagramBehavior;
}
/**
* This method is called to load the diagram into the editor. The default
* implementation here will use the {@link TransactionalEditingDomain} and
* its {@link ResourceSet} to load an EMF {@link Resource} that holds the
* {@link Diagram}. It will also enable modification tracking on the diagram
* {@link Resource}.
*
* @param uri
* the {@link URI} of the diagram to load
* @return the instance of the {@link Diagram} as it is resolved within the
* editor, meaning as it is resolved within the editor's
* {@link TransactionalEditingDomain}.
*/
public Diagram loadDiagram(URI uri) {
if (uri != null) {
final TransactionalEditingDomain editingDomain = diagramBehavior.getEditingDomain();
if (editingDomain != null) {
// First try the URI resolution without loading not yet loaded
// resources because calling with loadOnDemand will _always_
// create a new Resource instance for newly created and not yet
// saved Resources, no matter if they already exist within the
// ResourceSet or not
EObject modelElement = null;
// Catch exceptions that happen while loading the resource to
// avoid spamming the log and showing nasty messages to the user
// in the editor, see Bug 376008
try {
modelElement = editingDomain.getResourceSet().getEObject(uri, false);
if (modelElement == null) {
modelElement = editingDomain.getResourceSet().getEObject(uri, true);
if (modelElement == null) {
return null;
}
}
} catch (WrappedException e) {
// Log only if debug tracing is active to avoid user
// confusion (message is shown in the editor anyhow)
T.racer().debug("Diagram with URI '" + uri.toString() + "' could not be loaded", e); //$NON-NLS-1$ //$NON-NLS-2$
return null;
}
if (modelElement instanceof Diagram) {
modelElement.eResource().setTrackingModification(true);
return (Diagram) modelElement;
}
}
}
return null;
}
/**
* This method is called to save a diagram. The default implementation here
* saves all changes done to any of the EMF resources loaded within the
* {@link DiagramBehavior} so that the complete state of all modified
* objects will be persisted in the file system.<br>
* The default implementation also sets the current version information
* (currently 0.19.0) to the diagram before saving it and wraps the save
* operation inside a {@link IRunnableWithProgress} that cares about sending
* only one {@link Resource} change event holding all modified files.
* Besides also all adapters are temporarily switched off (see
* {@link DiagramBehavior#disableAdapters()}).<br>
* To only modify the actual saving clients should rather override
* {@link #save(TransactionalEditingDomain, Map)}.
*
* @param monitor
* the Eclipse {@link IProgressMonitor} to use to report progress
*/
public void saveDiagram(IProgressMonitor monitor) {
if (monitor == null) {
monitor = new NullProgressMonitor();
}
// set version info.
final Diagram diagram = diagramBehavior.getDiagramTypeProvider().getDiagram();
setDiagramVersion(diagram);
Map<Resource, Map<?, ?>> saveOptions = createSaveOptions();
final Set<Resource> savedResources = new HashSet<Resource>();
final IRunnableWithProgress operation = createOperation(savedResources, saveOptions);
diagramBehavior.disableAdapters();
try {
// This runs the options in a background thread reporting progress
// to the progress monitor passed into this method (see Bug 393074)
ModalContext.run(operation, true, monitor, Display.getDefault());
BasicCommandStack commandStack = (BasicCommandStack) diagramBehavior.getEditingDomain().getCommandStack();
commandStack.saveIsDone();
// Store the last executed command on the undo stack as save point
// and refresh the dirty state of the editor
savedCommand = commandStack.getUndoCommand();
diagramBehavior.getDiagramContainer().updateDirtyState();
} catch (InvocationTargetException e) {
if (e.getCause() instanceof SaveException) {
showSaveError(((SaveException) e.getCause()).getStatus());
} else {
T.racer().error("Save failed", e);
}
} catch (Exception exception) {
// Something went wrong that shouldn't.
T.racer().error(exception.getMessage(), exception);
} finally {
diagramBehavior.enableAdapters();
}
Resource[] savedResourcesArray = savedResources.toArray(new Resource[savedResources.size()]);
diagramBehavior.getDiagramContainer().commandStackChanged(null);
IDiagramTypeProvider provider = diagramBehavior.getConfigurationProvider().getDiagramTypeProvider();
provider.resourcesSaved(provider.getDiagram(), savedResourcesArray);
}
/**
* Returns if the editor needs to be saved or not. Is queried by the
* {@link DiagramBehavior#isDirty()} method. The default implementation
* checks if the top of the current undo stack is equal to the stored top
* command of the undo stack at the time of the last saving of the editor.
*
* @return <code>true</code> in case the editor needs to be saved,
* <code>false</code> otherwise.
*/
public boolean isDirty() {
TransactionalEditingDomain editingDomain = diagramBehavior.getEditingDomain();
if (editingDomain == null) {
// Right in the middle of closing the editor, it cannot be dirty
// without an editing domain
return false;
}
BasicCommandStack commandStack = (BasicCommandStack) editingDomain.getCommandStack();
if (commandStack == null) {
// Right in the middle of closing the editor, it cannot be dirty
// without a command stack
return false;
}
return savedCommand != commandStack.getUndoCommand();
}
/**
* Returns the EMF save options to be used when saving the EMF
* {@link Resource}s.
*
* @return a {@link Map} object holding the used EMF save options.
*/
protected Map<Resource, Map<?, ?>> createSaveOptions() {
// Save only resources that have actually changed.
final Map<Object, Object> saveOption = new HashMap<Object, Object>();
saveOption.put(Resource.OPTION_SAVE_ONLY_IF_CHANGED, Resource.OPTION_SAVE_ONLY_IF_CHANGED_MEMORY_BUFFER);
EList<Resource> resources = diagramBehavior.getEditingDomain().getResourceSet().getResources();
final Map<Resource, Map<?, ?>> saveOptions = new HashMap<Resource, Map<?, ?>>();
for (Resource resource : resources) {
saveOptions.put(resource, saveOption);
}
return saveOptions;
}
/**
* Creates the runnable to be used to wrap the actual saving of the EMF
* {@link Resource}s.<br>
* To only modify the actual saving clients should rather override
* {@link #save(TransactionalEditingDomain, Map)}.
*
* @param savedResources
* this parameter will after the operation has been performed
* contain all EMF {@link Resource}s that have really been saved.
*
* @param saveOptions
* the EMF save options to use.
* @return an {@link IRunnableWithProgress} instance wrapping the actual
* save process.
*/
protected IRunnableWithProgress createOperation(final Set<Resource> savedResources,
final Map<Resource, Map<?, ?>> saveOptions) {
// Do the work within an operation because this is a long running
// activity that modifies the workbench.
final IRunnableWithProgress operation = new SaveOperation(saveOptions, savedResources);
return operation;
}
/**
* Saves all resources in the given {@link TransactionalEditingDomain}. Can
* be overridden to enable additional (call the super method to save the EMF
* resources) or other persistencies.
*
* @param editingDomain
* the {@link TransactionalEditingDomain} for which all resources
* will be saved
* @param saveOptions
* the EMF save options used for the saving.
* @param monitor
* The progress monitor to use for reporting progress
* @return a {@link Set} of all EMF {@link Resource}s that were actually
* saved.
* @since 0.10 The parameter monitor has been added compared to the 0.9
* version of this method
*/
protected Set<Resource> save(final TransactionalEditingDomain editingDomain,
final Map<Resource, Map<?, ?>> saveOptions,
IProgressMonitor monitor) {
final Map<URI, Throwable> failedSaves = new HashMap<URI, Throwable>();
final Set<Resource> savedResources = new HashSet<Resource>();
final IWorkspaceRunnable wsRunnable = new IWorkspaceRunnable() {
public void run(final IProgressMonitor monitor) throws CoreException {
final Runnable runnable = new Runnable() {
public void run() {
Transaction parentTx;
if (editingDomain != null
&& (parentTx = ((InternalTransactionalEditingDomain) editingDomain)
.getActiveTransaction()) != null) {
do {
if (!parentTx.isReadOnly()) {
throw new IllegalStateException(
"saveInWorkspaceRunnable() called from within a command (likely to produce deadlock)"); //$NON-NLS-1$
}
} while ((parentTx = ((InternalTransactionalEditingDomain) editingDomain)
.getActiveTransaction().getParent()) != null);
}
final EList<Resource> resources = editingDomain.getResourceSet().getResources();
// Copy list to an array to prevent
// ConcurrentModificationExceptions during the saving of
// the dirty resources
Resource[] resourcesArray = new Resource[resources.size()];
resourcesArray = resources.toArray(resourcesArray);
for (int i = 0; i < resourcesArray.length; i++) {
// In case resource modification tracking is
// switched on, we can check if a resource has been
// modified, so that we only need to save really
// changed resources; otherwise we need to save all
// resources in the set
final Resource resource = resourcesArray[i];
if (shouldSave(resource)) {
try {
resource.save(saveOptions.get(resource));
savedResources.add(resource);
} catch (final Throwable t) {
failedSaves.put(resource.getURI(), t);
}
}
}
}
};
try {
editingDomain.runExclusive(runnable);
} catch (final InterruptedException e) {
throw new RuntimeException(e);
}
}
};
try {
ResourcesPlugin.getWorkspace().run(wsRunnable, null);
if (!failedSaves.isEmpty()) {
SaveException exception = createException(failedSaves);
IStatus[] statuses = exception.getStatus().getChildren();
for (IStatus status : statuses) {
T.racer().error("Resources could not be saved", status.getException());
}
throw exception;
}
} catch (final CoreException e) {
final Throwable cause = e.getStatus().getException();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
throw new RuntimeException(e);
}
return savedResources;
}
private SaveException createException(Map<URI, Throwable> failedSaves) {
MultiStatus multiStatus = new MultiStatus(GraphitiUIPlugin.PLUGIN_ID, 0,
Messages.DefaultPersistencyBehavior_2, null);
for (Entry<URI, Throwable> entry : failedSaves.entrySet()) {
IStatus status = new Status(IStatus.ERROR, GraphitiUIPlugin.PLUGIN_ID, entry.getKey().toString(),
entry.getValue());
multiStatus.add(status);
}
return new SaveException(multiStatus);
}
/**
* Called in {@link #saveDiagram(IProgressMonitor)} to update the Graphiti
* diagram version before saving a diagram. Currently the diagram version is
* set to 0.19.0
*
* @param diagram
* the {@link Diagram} to update the version attribute for
*/
protected void setDiagramVersion(final Diagram diagram) {
// Only trigger a command if the version really changes to avoid an
// empty entry in the command stack / undo stack
if (!IDiagramVersion.CURRENT.equals(diagram.getVersion())) {
CommandStack commandStack = diagramBehavior.getEditingDomain().getCommandStack();
commandStack.execute(new RecordingCommand(diagramBehavior.getEditingDomain()) {
@Override
protected void doExecute() {
diagram.eSet(PictogramsPackage.eINSTANCE.getDiagram_Version(), IDiagramVersion.CURRENT);
}
});
}
}
/**
* Checks whether a resource should be save during the diagram save process.
* By default, just passes the check to the editing domain.
*
* @param resource
* the {@link Resource} to check
* @return true if the resource must be saved, i.e., it's not read-only
*
* @since 0.11
*/
protected boolean shouldSave(Resource resource) {
/*
* Bug 371513 - Added check for isLoaded(): a resource that has not yet
* been loaded (possibly after a reload triggered by a change in another
* editor) has no content yet; saving such a resource will simply erase
* _all_ content from the resource on the disk (including the diagram).
* --> a not yet loaded resource must not be saved
*/
/*
* Bug 441676 - Removed check for isReadOnly on resource level. When
* read-only resources are saved an exception should be thrown to
* indicate to the user that something went wrong.
*/
return (!resource.isTrackingModification() || resource.isModified()) && resource.isLoaded();
}
/**
* @since 0.13
*/
protected void showSaveError(final IStatus status) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
new ErrorDialog(Display.getDefault().getActiveShell(), Messages.DefaultPersistencyBehavior_3,
Messages.DefaultPersistencyBehavior_4, status, IStatus.ERROR).open();
}
});
}
/**
* The workspace operation used to do the actual save.
*
* @since 0.11
*/
protected final class SaveOperation implements IRunnableWithProgress, IThreadListener {
private final Map<Resource, Map<?, ?>> saveOptions;
private final Set<Resource> savedResources;
private SaveOperation(Map<Resource, Map<?, ?>> saveOptions, Set<Resource> savedResources) {
this.saveOptions = saveOptions;
this.savedResources = savedResources;
}
// This is the method that gets invoked when the operation runs.
public void run(IProgressMonitor monitor) {
// Save the resources to the file system.
try {
savedResources.addAll(save(diagramBehavior.getEditingDomain(), saveOptions, monitor));
} catch (final WrappedException e) {
Status errorStatus = new Status(IStatus.ERROR, GraphitiUIPlugin.PLUGIN_ID, 0, e.getMessage(),
e.exception());
showSaveError(errorStatus);
T.racer().error(e.getMessage(), e.exception());
}
}
/*
* Transfer the rule from the calling thread to the callee. This should
* be invoked before executing the callee and after the callee has
* executed, thus transferring the rule back to the calling thread. See
* https://bugs.eclipse.org/bugs/show_bug.cgi?id=391046
*/
@Override
public void threadChange(Thread thread) {
ISchedulingRule rule = Job.getJobManager().currentRule();
if (rule != null) {
Job.getJobManager().transferRule(rule, thread);
}
}
}
/**
* @since 0.13
*/
protected final class SaveException extends RuntimeException {
private static final long serialVersionUID = 5342549194820431664L;
private MultiStatus status;
public SaveException(MultiStatus status) {
super(Messages.DefaultPersistencyBehavior_4);
this.status = status;
}
/**
* @return the status
*/
public MultiStatus getStatus() {
return status;
}
}
}