/****************************************************************************** * Copyright (c) 2000, 2006 IBM Corporation and others. * 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: * IBM Corporation - initial API and implementation ****************************************************************************/ package org.eclipse.gmf.runtime.diagram.ui.resources.editor.ide.document; import java.io.ByteArrayInputStream; import org.eclipse.core.filebuffers.manipulation.ContainerCreator; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceDeltaVisitor; import org.eclipse.core.resources.IResourceRuleFactory; import org.eclipse.core.resources.IResourceStatus; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.jface.operation.IRunnableContext; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.part.FileEditorInput; import org.eclipse.gmf.runtime.diagram.ui.resources.editor.document.IDocument; import org.eclipse.gmf.runtime.diagram.ui.resources.editor.ide.internal.l10n.EditorMessages; /** * Shared document provider specialized for file resources (<code>IFile</code>). * <p> * This class should be subclassed for different types of documents.</p> */ public abstract class FileDocumentProvider extends StorageDocumentProvider { /** * The runnable context for that provider. */ private WorkspaceOperationRunner fOperationRunner; /** * The scheduling rule factory. */ protected IResourceRuleFactory fResourceRuleFactory; /** * Runnable encapsulating an element state change. This runnable ensures * that a element change failed message is sent out to the element state listeners * in case an exception occurred. * * */ protected class SafeChange implements Runnable { /** The input that changes. */ private IFileEditorInput fInput; /** * Creates a new safe runnable for the given input. * * @param input the input */ public SafeChange(IFileEditorInput input) { fInput= input; } /** * Execute the change. * Subclass responsibility. * * @param input the input * @throws Exception an exception in case of error */ protected void execute(IFileEditorInput input) throws Exception { // overriden } /* * @see java.lang.Runnable#run() */ public void run() { if (getElementInfo(fInput) == null) { fireElementStateChangeFailed(fInput); return; } try { execute(fInput); } catch (Exception e) { fireElementStateChangeFailed(fInput); } } } /** * Synchronizes the document with external resource changes. */ protected class FileSynchronizer implements IResourceChangeListener, IResourceDeltaVisitor { /** The file editor input. */ protected IFileEditorInput fFileEditorInput; /** * A flag indicating whether this synchronizer is installed or not. * * */ protected boolean fIsInstalled= false; /** * Creates a new file synchronizer. Is not yet installed on a resource. * * @param fileEditorInput the editor input to be synchronized */ public FileSynchronizer(IFileEditorInput fileEditorInput) { fFileEditorInput= fileEditorInput; } /** * Returns the file wrapped by the file editor input. * * @return the file wrapped by the editor input associated with that synchronizer */ protected IFile getFile() { return fFileEditorInput.getFile(); } /** * Installs the synchronizer on the input's file. */ public void install() { getFile().getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE); fIsInstalled= true; } /** * Uninstalls the synchronizer from the input's file. */ public void uninstall() { getFile().getWorkspace().removeResourceChangeListener(this); fIsInstalled= false; } /* * @see IResourceChangeListener#resourceChanged(IResourceChangeEvent) */ public void resourceChanged(IResourceChangeEvent e) { IResourceDelta delta= e.getDelta(); try { if (delta != null && fIsInstalled) delta.accept(this); } catch (CoreException x) { handleCoreException(x, EditorMessages.FileDocumentProvider_resourceChanged); } } /* * @see IResourceDeltaVisitor#visit(org.eclipse.core.resources.IResourceDelta) */ public boolean visit(IResourceDelta delta) throws CoreException { if (delta == null) return false; delta= delta.findMember(getFile().getFullPath()); if (delta == null) return false; Runnable runnable= null; switch (delta.getKind()) { case IResourceDelta.CHANGED: FileInfo info= (FileInfo) getElementInfo(fFileEditorInput); if (info == null || info.fCanBeSaved) break; boolean isSynchronized= computeModificationStamp(getFile()) == info.fModificationStamp; if (((IResourceDelta.ENCODING & delta.getFlags()) != 0 && isSynchronized) || ((IResourceDelta.CONTENT & delta.getFlags()) != 0 && !isSynchronized)) { runnable = new SafeChange(fFileEditorInput) { protected void execute(IFileEditorInput input) throws Exception { handleElementContentChanged(input); } }; } break; case IResourceDelta.REMOVED: if ((IResourceDelta.MOVED_TO & delta.getFlags()) != 0) { final IPath path= delta.getMovedToPath(); runnable= new SafeChange(fFileEditorInput) { protected void execute(IFileEditorInput input) throws Exception { handleElementMoved(input, path); } }; } else { info= (FileInfo) getElementInfo(fFileEditorInput); if (info != null && !info.fCanBeSaved) { runnable= new SafeChange(fFileEditorInput) { protected void execute(IFileEditorInput input) throws Exception { handleElementDeleted(input); } }; } } break; } if (runnable != null) update(runnable); return false; } /** * Posts the update code "behind" the running operation. * * @param runnable the update code */ protected void update(Runnable runnable) { if (runnable instanceof SafeChange) fireElementStateChanging(fFileEditorInput); IWorkbench workbench= PlatformUI.getWorkbench(); IWorkbenchWindow[] windows= workbench.getWorkbenchWindows(); if (windows != null && windows.length > 0) { Display display= windows[0].getShell().getDisplay(); display.asyncExec(runnable); } else { runnable.run(); } } } /** * Bundle of all required information to allow files as underlying document resources. */ protected class FileInfo extends StorageInfo { /** The file synchronizer. */ public FileSynchronizer fFileSynchronizer; /** The time stamp at which this provider changed the file. */ public long fModificationStamp= IResource.NULL_STAMP; /** * Creates and returns a new file info. * * @param document the document * @param model the annotation model * @param fileSynchronizer the file synchronizer */ public FileInfo(IDocument document, FileSynchronizer fileSynchronizer) { super(document); fFileSynchronizer= fileSynchronizer; } } /** * Creates and returns a new document provider. */ public FileDocumentProvider() { super(); fResourceRuleFactory= ResourcesPlugin.getWorkspace().getRuleFactory(); } /** * Checks whether the given resource has been changed on the * local file system by comparing the actual time stamp with the * cached one. If the resource has been changed, a <code>CoreException</code> * is thrown. * * @param cachedModificationStamp the cached modification stamp * @param resource the resource to check * @throws org.eclipse.core.runtime.CoreException if resource has been changed on the file system */ protected void checkSynchronizationState(long cachedModificationStamp, IResource resource) throws CoreException { if (cachedModificationStamp != computeModificationStamp(resource)) { Status status= new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, IResourceStatus.OUT_OF_SYNC_LOCAL, EditorMessages.FileDocumentProvider_error_out_of_sync, null); throw new CoreException(status); } } /** * Computes the initial modification stamp for the given resource. * * @param resource the resource * @return the modification stamp */ protected long computeModificationStamp(IResource resource) { long modificationStamp= resource.getModificationStamp(); IPath path= resource.getLocation(); if (path == null) return modificationStamp; modificationStamp= path.toFile().lastModified(); return modificationStamp; } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.IDocumentProvider#getModificationStamp(java.lang.Object) */ public long getModificationStamp(Object element) { if (element instanceof IFileEditorInput) { IFileEditorInput input= (IFileEditorInput) element; return computeModificationStamp(input.getFile()); } return super.getModificationStamp(element); } /* * @see IDocumentProvider#getSynchronizationStamp(Object) */ public long getSynchronizationStamp(Object element) { if (element instanceof IFileEditorInput) { FileInfo info= (FileInfo) getElementInfo(element); if (info != null) return info.fModificationStamp; } return super.getSynchronizationStamp(element); } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.AbstractDocumentProvider#doSynchronize(java.lang.Object, org.eclipse.core.runtime.IProgressMonitor) */ protected void doSynchronize(Object element, IProgressMonitor monitor) throws CoreException { if (element instanceof IFileEditorInput) { IFileEditorInput input= (IFileEditorInput) element; FileInfo info= (FileInfo) getElementInfo(element); if (info != null) { if (info.fFileSynchronizer != null) { info.fFileSynchronizer.uninstall(); refreshFile(input.getFile(), monitor); info.fFileSynchronizer.install(); } else { refreshFile(input.getFile(), monitor); } handleElementContentChanged((IFileEditorInput) element); } return; } super.doSynchronize(element, monitor); } /* * @see IDocumentProvider#isDeleted(Object) */ public boolean isDeleted(Object element) { if (element instanceof IFileEditorInput) { IFileEditorInput input= (IFileEditorInput) element; IPath path= input.getFile().getLocation(); if (path == null) return true; return !path.toFile().exists(); } return super.isDeleted(element); } /** * Initializes the given document with the given stream using the given encoding. * * @param document the document to be initialized * @param contentStream the stream which delivers the document content * @param encoding the character encoding for reading the given stream * @throws CoreException if the given stream can not be read * */ protected abstract void saveDocumentToFile(IDocument document, IFile file, boolean overwrite, IProgressMonitor monitor) throws CoreException; /* * @see AbstractDocumentProvider#doSaveDocument(IProgressMonitor, Object, IDocument, boolean) */ protected void doSaveDocument(IProgressMonitor monitor, Object element, IDocument document, boolean overwrite) throws CoreException { if (element instanceof IFileEditorInput) { IFileEditorInput input= (IFileEditorInput) element; FileInfo info= (FileInfo) getElementInfo(element); IFile file= input.getFile(); if (file.exists()) { if (info != null && !overwrite) checkSynchronizationState(info.fModificationStamp, file); // inform about the upcoming content change fireElementStateChanging(element); try { saveDocumentToFile(document, file, overwrite, monitor); } catch (CoreException x) { // inform about failure fireElementStateChangeFailed(element); throw x; } catch (RuntimeException x) { // inform about failure fireElementStateChangeFailed(element); throw x; } // If here, the editor state will be flipped to "not dirty". // Thus, the state changing flag will be reset. if (info != null) { info.fModificationStamp= computeModificationStamp(file); } } else { try { monitor.beginTask(EditorMessages.FileDocumentProvider_task_saving, 3000); ContainerCreator creator = new ContainerCreator(file.getWorkspace(), file.getParent().getFullPath()); creator.createContainer(new SubProgressMonitor(monitor, 1000)); file.create(new ByteArrayInputStream("".getBytes()), false, new SubProgressMonitor(monitor, 1000)); //$NON-NLS-1$ saveDocumentToFile(document, file, overwrite, new SubProgressMonitor(monitor, 1000)); } finally { monitor.done(); } } } else { super.doSaveDocument(monitor, element, document, overwrite); } } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.AbstractDocumentProvider#createElementInfo(java.lang.Object) */ protected ElementInfo createElementInfo(Object element) throws CoreException { if (element instanceof IFileEditorInput) { IFileEditorInput input= (IFileEditorInput) element; try { refreshFile(input.getFile()); } catch (CoreException x) { handleCoreException(x, EditorMessages.FileDocumentProvider_createElementInfo); } IDocument d= null; IStatus s= null; try { d= createDocument(element); } catch (CoreException x) { handleCoreException(x, EditorMessages.FileDocumentProvider_createElementInfo); s= x.getStatus(); d= createEmptyDocument(); } FileSynchronizer f= new FileSynchronizer(input); f.install(); FileInfo info= createFileInfo(d, f, input); info.fModificationStamp= computeModificationStamp(input.getFile()); info.fStatus= s; return info; } return super.createElementInfo(element); } /** * Create a FileInfo for the given document. * * May also construct and start required listeners. * * @param document to create a FileInfo for * @param synchronizer FileSynchronizer which has been created for the * IFileEditorInput * @param input IFileEditorInput corresponding to the document * @return FileInfo for the given document */ protected FileInfo createFileInfo(IDocument document, FileSynchronizer synchronizer, IFileEditorInput input) { return new FileInfo(document, synchronizer); } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.AbstractDocumentProvider#disposeElementInfo(java.lang.Object, org.eclipse.gmf.runtime.diagram.ui.editor.AbstractDocumentProvider.ElementInfo) */ protected void disposeElementInfo(Object element, ElementInfo info) { if (info instanceof FileInfo) { FileInfo fileInfo= (FileInfo) info; if (fileInfo.fFileSynchronizer != null) fileInfo.fFileSynchronizer.uninstall(); } super.disposeElementInfo(element, info); } /** * Updates the element info to a change of the file content and sends out * appropriate notifications. * * @param fileEditorInput the input of a document editor */ protected void handleElementContentChanged(IFileEditorInput fileEditorInput) { FileInfo info= (FileInfo) getElementInfo(fileEditorInput); if (info == null) return; IDocument document= createEmptyDocument(); IStatus status= null; try { try { refreshFile(fileEditorInput.getFile()); } catch (CoreException x) { handleCoreException(x, EditorMessages.FileDocumentProvider_handleElementContentChanged); } setDocumentContent(document, fileEditorInput); } catch (CoreException x) { status= x.getStatus(); } Object newContent= document.getContent(); if ( !newContent.equals(info.fDocument.getContent())) { // set the new content and fire content related events fireElementContentAboutToBeReplaced(fileEditorInput); removeUnchangedElementListeners(fileEditorInput, info); info.fDocument.removeDocumentListener(info); info.fDocument.setContent(newContent); info.fCanBeSaved= false; info.fModificationStamp= computeModificationStamp(fileEditorInput.getFile()); info.fStatus= status; addUnchangedElementListeners(fileEditorInput, info); fireElementContentReplaced(fileEditorInput); } else { handleExistingDocumentSaved(fileEditorInput, info, status); } } /** * Called when an existing document matching the given IFileEditorInput * was saved. * * @param input IFileEditorInput for the document that was saved */ void handleExistingDocumentSaved(IFileEditorInput input) { ElementInfo info = getElementInfo(input); assert info instanceof FileInfo; handleExistingDocumentSaved(input, (FileInfo)info, null); } /** * Called when an existing document was saved. * * @param fileEditorInput IFileEditorInput for the document that was saved * @param info FileInfo for the given fileEditorInput element * @param status IStatus of the FileInfo */ private void handleExistingDocumentSaved(IFileEditorInput fileEditorInput, FileInfo info, IStatus status) { removeUnchangedElementListeners(fileEditorInput, info); // fires only the dirty state related event info.fCanBeSaved= false; info.fModificationStamp= computeModificationStamp(fileEditorInput.getFile()); info.fStatus= status; addUnchangedElementListeners(fileEditorInput, info); fireElementDirtyStateChanged(fileEditorInput, false); } /** * Initializes the given document with the given stream using the given encoding. * * @param document the document to be initialized * @param contentStream the stream which delivers the document content * @param encoding the character encoding for reading the given stream * @throws CoreException if the given stream can not be read * */ protected void setDocumentContent(IDocument document, Object content) throws CoreException { document.setContent(content); } /** * Sends out the notification that the file serving as document input has been moved. * * @param fileEditorInput the input of an document editor * @param path the path of the new location of the file */ protected void handleElementMoved(IFileEditorInput fileEditorInput, IPath path) { IWorkspace workspace= ResourcesPlugin.getWorkspace(); IFile newFile= workspace.getRoot().getFile(path); fireElementMoved(fileEditorInput, newFile == null ? null : new FileEditorInput(newFile)); } /** * Sends out the notification that the file serving as document input has been deleted. * * @param fileEditorInput the input of an document editor */ protected void handleElementDeleted(IFileEditorInput fileEditorInput) { fireElementDeleted(fileEditorInput); } /* * @see AbstractDocumentProvider#getElementInfo(Object) * It's only here to circumvent visibility issues with certain compilers. */ protected ElementInfo getElementInfo(Object element) { return super.getElementInfo(element); } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.AbstractDocumentProvider#doValidateState(java.lang.Object, java.lang.Object) */ protected void doValidateState(Object element, Object computationContext) throws CoreException { if (element instanceof IFileEditorInput) { IFileEditorInput input= (IFileEditorInput) element; FileInfo info= (FileInfo) getElementInfo(input); if (info != null) { IFile file= input.getFile(); if (file.isReadOnly()) { // do not use cached state here IWorkspace workspace= file.getWorkspace(); workspace.validateEdit(new IFile[] { file }, computationContext); } } } super.doValidateState(element, computationContext); } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.IDocumentProvider#isModifiable(java.lang.Object) */ public boolean isModifiable(Object element) { if (!isStateValidated(element)) { if (element instanceof IFileEditorInput) return true; } return super.isModifiable(element); } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.AbstractDocumentProvider#doResetDocument(java.lang.Object, org.eclipse.core.runtime.IProgressMonitor) */ protected void doResetDocument(Object element, IProgressMonitor monitor) throws CoreException { if (element instanceof IFileEditorInput) { IFileEditorInput input= (IFileEditorInput) element; try { refreshFile(input.getFile(), monitor); } catch (CoreException x) { handleCoreException(x,EditorMessages.FileDocumentProvider_resetDocument); } } super.doResetDocument(element, monitor); } /** * Refreshes the given file resource. * * @param file * @throws CoreException if the refresh fails * */ protected void refreshFile(IFile file) throws CoreException { refreshFile(file, getProgressMonitor()); } /** * Refreshes the given file resource. * * @param file the file to be refreshed * @param monitor the progress monitor * @throws org.eclipse.core.runtime.CoreException if the refresh fails * */ protected void refreshFile(IFile file, IProgressMonitor monitor) throws CoreException { try { file.refreshLocal(IResource.DEPTH_INFINITE, monitor); } catch (OperationCanceledException x) { // ignore } } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.IDocumentProvider#isSynchronized(java.lang.Object) */ public boolean isSynchronized(Object element) { if (element instanceof IFileEditorInput) { FileInfo info = null; if ((info = (FileInfo)getElementInfo(element)) != null) { IFileEditorInput input= (IFileEditorInput) element; IResource resource= input.getFile(); return (info.fModificationStamp == computeModificationStamp(resource)) && resource.isSynchronized(IResource.DEPTH_ZERO); } return false; } return super.isSynchronized(element); } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.AbstractDocumentProvider#getOperationRunner(org.eclipse.core.runtime.IProgressMonitor) */ protected IRunnableContext getOperationRunner(IProgressMonitor monitor) { if (fOperationRunner == null) fOperationRunner = new WorkspaceOperationRunner(); fOperationRunner.setProgressMonitor(monitor); return fOperationRunner; } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.AbstractDocumentProvider#getResetRule(java.lang.Object) */ protected ISchedulingRule getResetRule(Object element) { if (element instanceof IFileEditorInput) { IFileEditorInput input= (IFileEditorInput) element; return fResourceRuleFactory.modifyRule(input.getFile()); } return null; } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.AbstractDocumentProvider#getSaveRule(java.lang.Object) */ protected ISchedulingRule getSaveRule(Object element) { if (element instanceof IFileEditorInput) { IFileEditorInput input= (IFileEditorInput) element; return computeSchedulingRule(input.getFile()); } return null; } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.AbstractDocumentProvider#getSynchronizeRule(java.lang.Object) */ protected ISchedulingRule getSynchronizeRule(Object element) { if (element instanceof IFileEditorInput) { IFileEditorInput input= (IFileEditorInput) element; return fResourceRuleFactory.refreshRule(input.getFile()); } return null; } /* (non-Javadoc) * @see org.eclipse.gmf.runtime.diagram.ui.editor.AbstractDocumentProvider#getValidateStateRule(java.lang.Object) */ protected ISchedulingRule getValidateStateRule(Object element) { if (element instanceof IFileEditorInput) { IFileEditorInput input= (IFileEditorInput) element; return fResourceRuleFactory.validateEditRule(new IResource[] { input.getFile() }); } return null; } /** * Computes the scheduling rule needed to create or modify a resource. If * the resource exists, its modify rule is returned. If it does not, the * resource hierarchy is iterated towards the workspace root to find the * first parent of <code>toCreateOrModify</code> that exists. Then the * 'create' rule for the last non-existing resource is returned. * * @param toCreateOrModify the resource to create or modify * @return the minimal scheduling rule needed to modify or create a resource */ private ISchedulingRule computeSchedulingRule(IResource toCreateOrModify) { if (toCreateOrModify.exists()) return fResourceRuleFactory.modifyRule(toCreateOrModify); IResource parent= toCreateOrModify; do { /* * XXX This is a workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=67601 * IResourceRuleFactory.createRule should iterate the hierarchy itself. */ toCreateOrModify= parent; parent= toCreateOrModify.getParent(); } while (parent != null && !parent.exists()); return fResourceRuleFactory.createRule(toCreateOrModify); } }