blob: 2411e01f751884993cfecc7b9115aff293017fc8 [file] [log] [blame]
/**********************************************************************
Copyright (c) 2000, 2002 IBM Corp. and others.
All rights reserved. This program and the accompanying materials
are made available under the terms of the Common Public License v1.0
which accompanies this distribution, and is available at
http://www.eclipse.org/legal/cpl-v10.html
Contributors:
IBM Corporation - Initial implementation
**********************************************************************/
package org.eclipse.ui.editors.text;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import org.eclipse.swt.widgets.Display;
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.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.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ContainerGenerator;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.IElementStateListener;
import org.eclipse.ui.texteditor.ResourceMarkerAnnotationModel;
/**
* Shareable document provider specialized for file resources (<code>IFile</code>).<p>
* This class may be instantiated or be subclassed.
*/
public class FileDocumentProvider extends StorageDocumentProvider {
/**
* 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.
* @since 2.0
*/
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;
}
/**
* Subclass responsibility.
* @param input the input
* @exception Exception in case of error
*/
protected void execute(IFileEditorInput input) throws Exception {
}
/*
* @see java.lang.Runnable#run()
* @since 2.0
*/
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;
/**
* 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;
};
/**
* Creates a new file synchronizer. Is not yet installed on a resource.
* @param fileEditorInput the editor input to be synchronized
* @deprecated use FileSynchronizer(IFileEditorInput)
*/
public FileSynchronizer(FileEditorInput 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);
}
/**
* Uninstalls the synchronizer from the input's file.
*/
public void uninstall() {
getFile().getWorkspace().removeResourceChangeListener(this);
}
/*
* @see IResourceChangeListener#resourceChanged(IResourceChangeEvent)
*/
public void resourceChanged(IResourceChangeEvent e) {
IResourceDelta delta= e.getDelta();
try {
if (delta != null)
delta.accept(this);
} catch (CoreException x) {
handleCoreException(x, TextEditorMessages.getString("FileDocumentProvider.resourceChanged")); //$NON-NLS-1$
}
}
/*
* @see IResourceDeltaVisitor#visit(IResourceDelta)
*/
public boolean visit(IResourceDelta delta) throws CoreException {
if (delta != null && getFile().equals(delta.getResource())) {
Runnable runnable= null;
switch (delta.getKind()) {
case IResourceDelta.CHANGED:
if ((IResourceDelta.CONTENT & delta.getFlags()) != 0) {
FileInfo info= (FileInfo) getElementInfo(fFileEditorInput);
if (!info.fCanBeSaved && computeModificationStamp(getFile()) != info.fModificationStamp) {
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 {
FileInfo info= (FileInfo) getElementInfo(fFileEditorInput);
if (!info.fCanBeSaved) {
runnable= new SafeChange(fFileEditorInput) {
protected void execute(IFileEditorInput input) throws Exception {
handleElementDeleted(input);
}
};
}
}
break;
}
if (runnable != null)
update(runnable);
}
return true; // because we are sitting on files anyway
}
/**
* 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 a new file info.
* @param document the document
* @param model the annotation model
* @param fileSynchronizer the file synchronizer
*/
public FileInfo(IDocument document, IAnnotationModel model, FileSynchronizer fileSynchronizer) {
super(document, model);
fFileSynchronizer= fileSynchronizer;
}
};
/**
* Creates a new document provider.
*/
public FileDocumentProvider() {
super();
}
/**
* Overrides <code>StorageDocumentProvider#setDocumentContent(IDocument, IEditorInput)</code>.
* @deprecated use file encoding based version
* @since 2.0
*/
protected boolean setDocumentContent(IDocument document, IEditorInput editorInput) throws CoreException {
if (editorInput instanceof IFileEditorInput) {
IFile file= ((IFileEditorInput) editorInput).getFile();
setDocumentContent(document, file.getContents(false));
return true;
}
return super.setDocumentContent(document, editorInput);
}
/*
* @see StorageDocumentProvider#setDocumentContent(IDocument, IEditorInput, String)
* @since 2.0
*/
protected boolean setDocumentContent(IDocument document, IEditorInput editorInput, String encoding) throws CoreException {
if (editorInput instanceof IFileEditorInput) {
IFile file= ((IFileEditorInput) editorInput).getFile();
setDocumentContent(document, file.getContents(false), encoding);
return true;
}
return super.setDocumentContent(document, editorInput, encoding);
}
/*
* @see AbstractDocumentProvider#createAnnotationModel(Object)
*/
protected IAnnotationModel createAnnotationModel(Object element) throws CoreException {
if (element instanceof IFileEditorInput) {
IFileEditorInput input= (IFileEditorInput) element;
return new ResourceMarkerAnnotationModel(input.getFile());
}
return super.createAnnotationModel(element);
}
/**
* 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 chached modification stamp
* @param resource the resource to check
* @exception 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, TextEditorMessages.getString("FileDocumentProvider.error.out_of_sync"), null); //$NON-NLS-1$
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;
}
/*
* @see IDocumentProvider#getModificationStamp(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);
return info.fModificationStamp;
}
return super.getSynchronizationStamp(element);
}
/*
* @see org.eclipse.ui.texteditor.IDocumentProviderExtension#synchronize(Object)
* @since 2.0
*/
public void synchronize(Object element) throws CoreException {
if (element instanceof IFileEditorInput) {
IFileEditorInput input= (IFileEditorInput) element;
FileInfo info= (FileInfo) getElementInfo(element);
if (info != null) {
info.fFileSynchronizer.uninstall();
input.getFile().refreshLocal(IResource.DEPTH_INFINITE, null);
info.fFileSynchronizer.install();
handleElementContentChanged((IFileEditorInput) element);
}
return;
}
super.synchronize(element);
}
/*
* @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);
}
/*
* @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;
try {
InputStream stream= new ByteArrayInputStream(document.get().getBytes(ResourcesPlugin.getEncoding()));
IFile file= input.getFile();
if (file.exists()) {
FileInfo info= (FileInfo) getElementInfo(element);
if (info != null && !overwrite)
checkSynchronizationState(info.fModificationStamp, file);
// inform about the upcoming content change
fireElementStateChanging(element);
try {
file.setContents(stream, overwrite, true, 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) {
ResourceMarkerAnnotationModel model= (ResourceMarkerAnnotationModel) info.fModel;
model.updateMarkers(info.fDocument);
info.fModificationStamp= computeModificationStamp(file);
}
} else {
try {
monitor.beginTask(TextEditorMessages.getString("FileDocumentProvider.task.saving"), 2000); //$NON-NLS-1$
ContainerGenerator generator = new ContainerGenerator(file.getParent().getFullPath());
generator.generateContainer(new SubProgressMonitor(monitor, 1000));
file.create(stream, false, new SubProgressMonitor(monitor, 1000));
}
finally {
monitor.done();
}
}
} catch (IOException x) {
IStatus s= new Status(IStatus.ERROR, PlatformUI.PLUGIN_ID, IStatus.OK, x.getMessage(), x);
throw new CoreException(s);
}
} else {
super.doSaveDocument(monitor, element, document, overwrite);
}
}
/*
* @see AbstractDocumentProvider#createElementInfo(Object)
*/
protected ElementInfo createElementInfo(Object element) throws CoreException {
if (element instanceof IFileEditorInput) {
IFileEditorInput input= (IFileEditorInput) element;
try {
input.getFile().refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
} catch (CoreException x) {
handleCoreException(x,TextEditorMessages.getString("FileDocumentProvider.createElementInfo")); //$NON-NLS-1$
}
IDocument d= null;
IStatus s= null;
try {
d= createDocument(element);
} catch (CoreException x) {
s= x.getStatus();
d= new Document();
}
IAnnotationModel m= createAnnotationModel(element);
FileSynchronizer f= new FileSynchronizer(input);
f.install();
FileInfo info= new FileInfo(d, m, f);
info.fModificationStamp= computeModificationStamp(input.getFile());
info.fStatus= s;
return info;
}
return super.createElementInfo(element);
}
/*
* @see AbstractDocumentProvider#disposeElementInfo(Object, 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 an text editor
*/
protected void handleElementContentChanged(IFileEditorInput fileEditorInput) {
FileInfo info= (FileInfo) getElementInfo(fileEditorInput);
IDocument document= new Document();
IStatus status= null;
try {
try {
fileEditorInput.getFile().refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
} catch (CoreException x) {
handleCoreException(x, "FileDocumentProvider.handleElementContentChanged"); //$NON-NLS-1$
}
setDocumentContent(document, fileEditorInput, info.fEncoding);
} catch (CoreException x) {
status= x.getStatus();
}
String newContent= document.get();
if ( !newContent.equals(info.fDocument.get())) {
// set the new content and fire content related events
fireElementContentAboutToBeReplaced(fileEditorInput);
removeUnchangedElementListeners(fileEditorInput, info);
info.fDocument.removeDocumentListener(info);
info.fDocument.set(newContent);
info.fCanBeSaved= false;
info.fModificationStamp= computeModificationStamp(fileEditorInput.getFile());
info.fStatus= status;
addUnchangedElementListeners(fileEditorInput, info);
fireElementContentReplaced(fileEditorInput);
} else {
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);
}
}
/**
* Sends out the notification that the file serving as document input has been moved.
*
* @param fileEditorInput the input of an text 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 text 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);
}
/*
* @see AbstractDocumentProvider#doValidateState(Object, Object)
* @since 2.0
*/
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);
}
/*
* @see IDocumentProviderExtension#isModifiable(Object)
* @since 2.0
*/
public boolean isModifiable(Object element) {
if (!isStateValidated(element)) {
if (element instanceof IFileEditorInput)
return true;
}
return super.isModifiable(element);
}
/*
* @see IDocumentProvider#resetDocument(Object)
* @since 2.0
*/
public void resetDocument(Object element) throws CoreException {
// http://dev.eclipse.org/bugs/show_bug.cgi?id=19014
if (element instanceof IFileEditorInput) {
IFileEditorInput input= (IFileEditorInput) element;
try {
input.getFile().refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
} catch (CoreException x) {
handleCoreException(x,TextEditorMessages.getString("FileDocumentProvider.resetDocument")); //$NON-NLS-1$
}
}
super.resetDocument(element);
}
}