/***************************************************************************** * Copyright (c) 2010 LIFL & CEA LIST. * * * 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: * Cedric Dumoulin (LIFL) cedric.dumoulin@lifl.fr - Initial API and implementation * *****************************************************************************/ package org.eclipse.papyrus.infra.core.lifecycleevents; import static org.eclipse.papyrus.infra.core.Activator.log; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.EventObject; import java.util.List; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; 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.command.CommandStackListener; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.transaction.NotificationFilter; import org.eclipse.emf.transaction.ResourceSetChangeEvent; import org.eclipse.emf.transaction.ResourceSetListener; import org.eclipse.emf.transaction.RollbackException; import org.eclipse.emf.transaction.TransactionalEditingDomain; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.papyrus.infra.core.editor.IMultiDiagramEditor; import org.eclipse.papyrus.infra.core.resource.ModelSet; import org.eclipse.papyrus.infra.core.services.IService; import org.eclipse.papyrus.infra.core.services.ServiceException; import org.eclipse.papyrus.infra.core.services.ServicesRegistry; import org.eclipse.papyrus.infra.core.utils.ServiceUtils; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.ISaveablePart; import org.eclipse.ui.actions.WorkspaceModifyOperation; import org.eclipse.ui.dialogs.SaveAsDialog; import org.eclipse.ui.part.FileEditorInput; /** * A Papyrus Service allowing to perform save and saveAs on Papyrus Models. The * service also allows to listen on the dirty state of the Models. <br> * The service implements the {@link ISaveablePart} interface, and can be used * directly in part requiring such interface of adapter. * * <br> * This class allows nested editors to register themselves as nested {@link ISaveablePart}. In this case, the registered part will be notified * each time a save or saveAs is performed. Also, the nested part will be asked * for its dirtyState. * * TODO : Improve the implementation by registering the isDirty flag value, and * firing events only if the value really change. Actually, the event is fired * every time the model is modified, even if the virtual value of the flag * hasn't changed. * * @author cedric dumoulin * */ public class SaveAndDirtyService extends LifeCycleEventsProvider implements ISaveablePart, IService, ISaveAndDirtyService { /** * Class used to propagate life cycle events. This class can be retrieved as * a service using {@link ILifeCycleEventsProvider}.class. This class * extends LifeCycleEventsProvider, so the local variable is set with * ourself (historical reasons). TODO : remove this local variable. */ protected LifeCycleEventsProvider lifeCycleEventsProvider = this; /** * Cached event that can be reused. */ protected DoSaveEvent lifeCycleEvent; /** * Model set managing models. */ private ModelSet resourceSet; /** * */ private TransactionalEditingDomain transactionalEditingDomain; /** * The serviceRegistry. */ // private ServicesRegistry servicesRegistry; /** * Associated editor. Needed by saveAs to synchronize editor input. */ private IMultiDiagramEditor multiDiagramEditor; /** * List of registered {@link ISaveablePart}. This are usually nested * editors. */ private ISaveablePartList registeredIsaveablePart; /** * List of listeners on input changed event after a call to saveAs. */ private List<IEditorInputChangedListener> inputChangedListeners; /** * Listener on commandStack changes. */ private final CommandStackListener commandStackListener = new CommandStackListener() { public void commandStackChanged(EventObject event) { fireIsDirtyChanged(); }; }; /* * Listener on ResourceSet */ private final ResourceSetListener resourceSetListener = new ResourceSetListener() { public NotificationFilter getFilter() { return null; } public boolean isAggregatePrecommitListener() { return false; } public boolean isPostcommitOnly() { return true; } public boolean isPrecommitOnly() { return false; } public void resourceSetChanged(ResourceSetChangeEvent event) { if(event.getTransaction() != null && event.getTransaction().getStatus().isOK()) { fireIsDirtyChanged(); } } public Command transactionAboutToCommit(ResourceSetChangeEvent event) throws RollbackException { return null; } }; /** * Constructor. * */ public SaveAndDirtyService() { registeredIsaveablePart = new ISaveablePartList(); inputChangedListeners = new ArrayList<IEditorInputChangedListener>(); } /** * Initialize the service. Retrieve other required services (ModelSet, * CoreEditor). * * @see org.eclipse.papyrus.infra.core.services.IService#init(org.eclipse.papyrus.infra.core.services.ServicesRegistry) * * @param servicesRegistry * @throws ServiceException */ public void init(ServicesRegistry servicesRegistry) throws ServiceException { // this.servicesRegistry = servicesRegistry; // Retrieve required services. resourceSet = servicesRegistry.getService(ModelSet.class); multiDiagramEditor = servicesRegistry.getService(IMultiDiagramEditor.class); transactionalEditingDomain = ServiceUtils.getInstance().getTransactionalEditingDomain(servicesRegistry); // Initialize and register the ILifeCycleEventsProvider service (which // is ourself). // This mean that the ILifeCycleEventsProvider is not available until we // are started. lifeCycleEvent = new DoSaveEvent(servicesRegistry, multiDiagramEditor); // servicesRegistry.add(ILifeCycleEventsProvider.class, 1, // lifeCycleEventsProvider); } /** * Do nothing. * * @see org.eclipse.papyrus.infra.core.services.IService#startService() * * @throws ServiceException */ public void startService() throws ServiceException { // Listen to the modifications of the EMF model transactionalEditingDomain.getCommandStack().addCommandStackListener(commandStackListener); // Let's listen to the resource set change transactionalEditingDomain.addResourceSetListener(resourceSetListener); } /** * @see org.eclipse.papyrus.infra.core.services.IService#disposeService() * * @throws ServiceException */ public void disposeService() throws ServiceException { if(transactionalEditingDomain != null) { // Check if commandStack is null (meaning that transactionalEditingDomain // is disposed CommandStack commandStack = transactionalEditingDomain.getCommandStack(); if( commandStack != null) { transactionalEditingDomain.getCommandStack().removeCommandStackListener(commandStackListener); } transactionalEditingDomain.removeResourceSetListener(resourceSetListener); // resourceSetListener = null; } // clean properties in order to help GC inputChangedListeners.clear(); inputChangedListeners = null; multiDiagramEditor = null; // servicesRegistry = null; transactionalEditingDomain = null; resourceSet = null; lifeCycleEvent = null; postSaveListeners.clear(); saveListeners.clear(); preSaveListeners.clear(); } /** * Save the Models * * @see org.eclipse.ui.ISaveablePart#doSave(org.eclipse.core.runtime.IProgressMonitor) * * @param monitor */ public void doSave(IProgressMonitor monitor) { // Sent pre doSave event lifeCycleEventsProvider.fireAboutToDoSaveEvent(lifeCycleEvent); // sent doSaveEvent lifeCycleEventsProvider.fireDoSaveEvent(lifeCycleEvent); // Perform local doSave // TODO : put it in a listener ? try { // Save each associated resource resourceSet.save(monitor); // notify registered IsaveablePart registeredIsaveablePart.doSave(monitor); markSaveLocation(); } catch (IOException e) { log.error("Error during save", e); } // Sent post Events lifeCycleEventsProvider.firePostDoSaveEvent(lifeCycleEvent); } /** * @see org.eclipse.ui.ISaveablePart#doSaveAs() * */ public void doSaveAs() { // Sent pre doSave event lifeCycleEventsProvider.fireAboutToDoSaveAsEvent(lifeCycleEvent); // sent doSaveEvent lifeCycleEventsProvider.fireDoSaveAsEvent(lifeCycleEvent); // Perform local doSaveAs // Show a SaveAs dialog Shell shell = multiDiagramEditor.getEditorSite().getWorkbenchWindow().getShell(); SaveAsDialog dialog = new SaveAsDialog(shell); dialog.setOriginalFile(((IFileEditorInput)multiDiagramEditor.getEditorInput()).getFile()); dialog.open(); final IPath path = dialog.getResult(); if(path != null) { // try to save the editor's contents under a different file name final IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(path); try { new ProgressMonitorDialog(shell).run(false, // don't fork false, // can't cancel new WorkspaceModifyOperation() { // run this operation @Override public void execute(final IProgressMonitor monitor) { try { //to event bad redirection after the saveAs //see bug 319023 EcoreUtil.resolveAll(resourceSet); resourceSet.saveAs(path); // notify registered IsaveablePart registeredIsaveablePart.doSave(monitor); } catch (IOException e) { log.error("Unable to saveAs the resource set", e); } } }); // set input to the new file fireEditorInputChanged(new FileEditorInput(file)); markSaveLocation(); } catch (InterruptedException e) { // should not happen, since the monitor dialog is not cancelable log.error(e); } catch (InvocationTargetException e) { log.error(e); } } // sent doSaveEvent lifeCycleEventsProvider.firePostDoSaveAsEvent(lifeCycleEvent); } /** * Change the input of the underlying editor. * * @param fileEditorInput */ private void fireEditorInputChanged(FileEditorInput fileEditorInput) { for(IEditorInputChangedListener listener : inputChangedListeners) { try { listener.editorInputChanged(fileEditorInput); } catch (Exception e) { log.error("Can't set input for '" + listener + "'", e); } } } /** * Fire a PropertyChanged event to registered {@link IEditorInputChangedListener}. * * @param propertyId */ private void fireIsDirtyChanged() { for(IEditorInputChangedListener listener : inputChangedListeners) { try { listener.isDirtyChanged(); } catch (Exception e) { log.error("Can't call listener '" + listener + "'", e); } } } /** * Return true if the multiEditor is dirty, false otherwise. The dirty state * is compute as follow: * <ul> * <li>The {@link TransactionalEditingDomain} commandStack is checked</li> * <li>and each registered nested Isaveable.isDirty() state is checked</li> * <li></li> * <li></li> * <li></li> * <li></li> * </ul> * If one of these states is false, the returned value is false. <br> * If all of these states are true, the returned value is true. * * @see org.eclipse.ui.ISaveablePart#isDirty() * * @return */ public boolean isDirty() { // First, look if the model part (EMF) is dirty, else look at the // Graphical part (GEF/GMF) return ((BasicCommandStack)transactionalEditingDomain.getCommandStack()).isSaveNeeded() || registeredIsaveablePart.isDirty(); } /** * @see org.eclipse.ui.ISaveablePart#isSaveAsAllowed() * * @return */ public boolean isSaveAsAllowed() { return true; } /** * @see org.eclipse.ui.ISaveablePart#isSaveOnCloseNeeded() * * @return */ public boolean isSaveOnCloseNeeded() { return isDirty(); } /** * Mark the command stack of all sub-editors. Default implementation do * nothing. */ protected void markSaveLocation() { ((BasicCommandStack)transactionalEditingDomain.getCommandStack()).saveIsDone(); fireIsDirtyChanged(); } /** * Register a nested {@link ISaveablePart} as a listener that will be * notified each time a {@link #doSave(IProgressMonitor)} or {@link #doSaveAs()} is performed. Also, it will be asked for the * dirtyState. * * @param saveablePart */ public void registerIsaveablePart(ISaveablePart saveablePart) { registeredIsaveablePart.add(saveablePart); } /** * Remove the specified {@link ISaveablePart} from the list of listeners. * * @param saveablePart */ public void removeIsaveablePart(ISaveablePart saveablePart) { registeredIsaveablePart.remove(saveablePart); } /** * Add a listeners on input changed event. * * @param inputChangedListener */ public void addInputChangedListener(IEditorInputChangedListener inputChangedListener) { inputChangedListeners.add(inputChangedListener); } /** * Remove a listeners on input changed event. * * @param inputChangedListener */ public void removeInputChangedListener(IEditorInputChangedListener inputChangedListener) { /* apex improved start */ // MultiDiagramEditor가 열려있는 상태에서 Eclipse를 닫는 경우 // MultiDiagramEditor.dispose()에 의해 SaveAndDirtyService.disposeService()가 먼저 호출되어 // inputChangedListeners가 이미 null 인 경우도 있으므로 아래 추가 if ( inputChangedListeners != null ) { inputChangedListeners.remove(inputChangedListener); } /* apex improved end */ /* apex replaced inputChangedListeners.remove(inputChangedListener); */ } /** * A list of {@link ISaveablePart}. * * @author dumoulin * */ public class ISaveablePartList extends ArrayList<ISaveablePart> { /** * */ private static final long serialVersionUID = 1L; /** * Return true if one of the part is dirty, false if all part are not * dirty. * * @return */ public boolean isDirty() { for(ISaveablePart part : this) { if(part.isDirty()) return true; } return false; } /** * Call doSave on each registered {@link ISaveablePart}. * * @param monitor */ public void doSave(IProgressMonitor monitor) { for(ISaveablePart part : this) { try { part.doSave(monitor); } catch (Exception e) { log.error("Can't save ISaveablePart '" + part + "'", e); } } } /** * Call doSaveAs on each registered {@link ISaveablePart}. * * @param monitor */ public void doSaveAs() { for(ISaveablePart part : this) { try { part.doSaveAs(); } catch (Exception e) { log.error("Can't save ISaveablePart '" + part + "'", e); } } } } }