/******************************************************************************* * <copyright> * * Copyright (c) 2011, 2013 SAP AG. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * 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 * * </copyright> * *******************************************************************************/ package org.eclipse.graphiti.ui.editor; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; 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.MultiStatus; import org.eclipse.core.runtime.NullProgressMonitor; 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.T; 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; } 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.11.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 (final 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() { BasicCommandStack commandStack = (BasicCommandStack) diagramBehavior.getEditingDomain().getCommandStack(); 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()) { throw new WrappedException(createMessage(failedSaves), new RuntimeException()); } } catch (final CoreException e) { final Throwable cause = e.getStatus().getException(); if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } throw new RuntimeException(e); } return savedResources; } private String createMessage(Map<URI, Throwable> failedSaves) { final StringBuilder buf = new StringBuilder("The following resources could not be saved:"); //$NON-NLS-1$ for (final Entry<URI, Throwable> entry : failedSaves.entrySet()) { buf.append("\nURI: ").append(entry.getKey().toString()).append(", cause: \n").append(getExceptionAsString(entry.getValue())); //$NON-NLS-1$ //$NON-NLS-2$ } return buf.toString(); } private String getExceptionAsString(Throwable t) { final StringWriter stringWriter = new StringWriter(); final PrintWriter printWriter = new PrintWriter(stringWriter); t.printStackTrace(printWriter); final String result = stringWriter.toString(); try { stringWriter.close(); } catch (final IOException e) { // $JL-EXC$ ignore } printWriter.close(); return result; } /** * Called in {@link #saveDiagram(IProgressMonitor)} to update the Graphiti * diagram version before saving a diagram. Currently the diagram version is * set to 0.11.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 */ return !diagramBehavior.getEditingDomain().isReadOnly(resource) && (!resource.isTrackingModification() || resource.isModified()) && resource.isLoaded(); } /** * 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) { final MultiStatus errorStatus = new MultiStatus(GraphitiUIPlugin.PLUGIN_ID, 0, e.getMessage(), e.exception()); GraphitiUIPlugin.getDefault().getLog().log(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); } } } }