| /******************************************************************************* |
| * Copyright (c) 2000, 2019 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.ui.internal.editors.quickdiff; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.nio.charset.StandardCharsets; |
| |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Shell; |
| |
| import org.eclipse.core.runtime.CoreException; |
| 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.content.IContentDescription; |
| import org.eclipse.core.runtime.jobs.IJobManager; |
| import org.eclipse.core.runtime.jobs.ISchedulingRule; |
| import org.eclipse.core.runtime.jobs.Job; |
| |
| import org.eclipse.core.resources.IEncodedStorage; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IStorage; |
| |
| import org.eclipse.jface.text.Document; |
| import org.eclipse.jface.text.IDocument; |
| |
| import org.eclipse.ui.IEditorInput; |
| import org.eclipse.ui.IStorageEditorInput; |
| import org.eclipse.ui.IWorkbenchPartSite; |
| import org.eclipse.ui.IWorkbenchWindow; |
| |
| import org.eclipse.ui.texteditor.IDocumentProvider; |
| import org.eclipse.ui.texteditor.IElementStateListener; |
| import org.eclipse.ui.texteditor.ITextEditor; |
| import org.eclipse.ui.texteditor.quickdiff.IQuickDiffReferenceProvider; |
| |
| import org.eclipse.ui.editors.text.EditorsUI; |
| import org.eclipse.ui.editors.text.IStorageDocumentProvider; |
| |
| /** |
| * Default provider for the quickdiff display - the saved document is taken as |
| * the reference. |
| * |
| * @since 3.0 |
| */ |
| public class LastSaveReferenceProvider implements IQuickDiffReferenceProvider, IElementStateListener { |
| |
| /** <code>true</code> if the document has been read. */ |
| private boolean fDocumentRead= false; |
| /** |
| * The reference document - might be <code>null</code> even if <code>fDocumentRead</code> |
| * is <code>true</code>. |
| */ |
| private IDocument fReference= null; |
| /** |
| * Our unique id that makes us comparable to another instance of the same |
| * provider. See extension point reference. |
| */ |
| private String fId; |
| /** The current document provider. */ |
| private IDocumentProvider fDocumentProvider; |
| /** The current editor input. */ |
| private IEditorInput fEditorInput; |
| /** Private lock no one else will synchronize on. */ |
| private final Object fLock= new Object(); |
| /** The document lock for non-IResources. */ |
| private final Object fDocumentAccessorLock= new Object(); |
| /** Document lock state, protected by <code>fDocumentAccessorLock</code>. */ |
| private boolean fDocumentLocked; |
| /** |
| * The progress monitor for a currently running <code>getReference</code> |
| * operation, or <code>null</code>. |
| */ |
| private IProgressMonitor fProgressMonitor; |
| /** The text editor we run upon. */ |
| private ITextEditor fEditor; |
| |
| /** |
| * A job to put the reading of file contents into a background. |
| */ |
| private final class ReadJob extends Job { |
| |
| /** |
| * Creates a new instance. |
| */ |
| public ReadJob() { |
| super(QuickDiffMessages.getString("LastSaveReferenceProvider.LastSaveReferenceProvider.readJob.label")); //$NON-NLS-1$ |
| setSystem(true); |
| setPriority(SHORT); |
| } |
| |
| /** |
| * Calls {@link LastSaveReferenceProvider#readDocument(IProgressMonitor, boolean)} and |
| * returns {@link Status#OK_STATUS}. |
| * |
| * {@inheritDoc} |
| * |
| * @param monitor {@inheritDoc} |
| * @return {@link Status#OK_STATUS} |
| */ |
| @Override |
| protected IStatus run(IProgressMonitor monitor) { |
| readDocument(monitor, false); |
| return Status.OK_STATUS; |
| } |
| } |
| |
| @Override |
| public IDocument getReference(IProgressMonitor monitor) { |
| if (!fDocumentRead) |
| readDocument(monitor, true); // force reading it |
| return fReference; |
| } |
| |
| @Override |
| public void dispose() { |
| IProgressMonitor monitor= fProgressMonitor; |
| if (monitor != null) { |
| monitor.setCanceled(true); |
| } |
| |
| IDocumentProvider provider= fDocumentProvider; |
| |
| synchronized (fLock) { |
| if (provider != null) |
| provider.removeElementStateListener(this); |
| fEditorInput= null; |
| fDocumentProvider= null; |
| fReference= null; |
| fDocumentRead= false; |
| fProgressMonitor= null; |
| fEditor= null; |
| } |
| } |
| |
| @Override |
| public String getId() { |
| return fId; |
| } |
| |
| @Override |
| public void setActiveEditor(ITextEditor targetEditor) { |
| IDocumentProvider provider= null; |
| IEditorInput input= null; |
| if (targetEditor != null) { |
| provider= targetEditor.getDocumentProvider(); |
| input= targetEditor.getEditorInput(); |
| } |
| |
| |
| // dispose if the editor input or document provider have changed |
| // note that they may serve multiple editors |
| if (provider != fDocumentProvider || input != fEditorInput) { |
| dispose(); |
| synchronized (fLock) { |
| fEditor= targetEditor; |
| fDocumentProvider= provider; |
| fEditorInput= input; |
| } |
| } |
| } |
| |
| @Override |
| public boolean isEnabled() { |
| return fEditorInput != null && fDocumentProvider != null; |
| } |
| |
| @Override |
| public void setId(String id) { |
| fId= id; |
| } |
| |
| /** |
| * Reads in the saved document into <code>fReference</code>. |
| * |
| * @param monitor a progress monitor, or <code>null</code> |
| * @param force <code>true</code> if the reference document should also |
| * be read if the current document is <code>null</code>,<code>false</code> |
| * if it should only be updated if it already existed. |
| */ |
| private void readDocument(IProgressMonitor monitor, boolean force) { |
| |
| // protect against concurrent disposal |
| IDocumentProvider prov= fDocumentProvider; |
| IEditorInput inp= fEditorInput; |
| IDocument doc= fReference; |
| ITextEditor editor= fEditor; |
| |
| if (prov instanceof IStorageDocumentProvider && inp instanceof IStorageEditorInput) { |
| |
| IStorageEditorInput input= (IStorageEditorInput) inp; |
| IStorageDocumentProvider provider= (IStorageDocumentProvider) prov; |
| |
| if (doc == null) |
| if (force || fDocumentRead) |
| doc= new Document(); |
| else |
| return; |
| |
| IJobManager jobMgr= Job.getJobManager(); |
| |
| try { |
| IStorage storage= input.getStorage(); |
| // check for null for backward compatibility (we used to check before...) |
| if (storage == null) |
| return; |
| fProgressMonitor= monitor; |
| ISchedulingRule rule= getSchedulingRule(storage); |
| |
| // this protects others from not being able to delete the file, |
| // and protects ourselves from concurrent access to fReference |
| // (in the case there already is a valid fReference) |
| |
| // one might argue that this rule should already be in the Job |
| // description we're running in, however: |
| // 1) we don't mind waiting for someone else here |
| // 2) we do not take long, or require other locks etc. -> short |
| // delay for any other job requiring the lock on file |
| try { |
| lockDocument(monitor, jobMgr, rule); |
| |
| String encoding; |
| if (storage instanceof IEncodedStorage) |
| encoding= ((IEncodedStorage) storage).getCharset(); |
| else |
| encoding= null; |
| |
| boolean skipUTF8BOM= isUTF8BOM(encoding, storage); |
| |
| setDocumentContent(doc, storage, encoding, monitor, skipUTF8BOM); |
| } finally { |
| unlockDocument(jobMgr, rule); |
| fProgressMonitor= null; |
| } |
| |
| } catch (CoreException e) { |
| return; |
| } |
| |
| if (monitor != null && monitor.isCanceled()) |
| return; |
| |
| // update state |
| synchronized (fLock) { |
| if (fDocumentProvider == provider && fEditorInput == input) { |
| // only update state if our provider / input pair has not |
| // been updated in between (dispose or setActiveEditor) |
| fReference= doc; |
| fDocumentRead= true; |
| addElementStateListener(editor, prov); |
| } |
| } |
| } |
| } |
| |
| private ISchedulingRule getSchedulingRule(IStorage storage) { |
| if (storage instanceof ISchedulingRule) |
| return (ISchedulingRule) storage; |
| else if (storage != null) |
| return storage.getAdapter(ISchedulingRule.class); |
| return null; |
| } |
| |
| /* utility methods */ |
| |
| private void lockDocument(IProgressMonitor monitor, IJobManager jobMgr, ISchedulingRule rule) { |
| if (rule != null) { |
| jobMgr.beginRule(rule, monitor); |
| } else synchronized (fDocumentAccessorLock) { |
| while (fDocumentLocked) { |
| try { |
| fDocumentAccessorLock.wait(); |
| } catch (InterruptedException e) { |
| // nobody interrupts us! |
| throw new OperationCanceledException(); |
| } |
| } |
| fDocumentLocked= true; |
| } |
| } |
| |
| private void unlockDocument(IJobManager jobMgr, ISchedulingRule rule) { |
| if (rule != null) { |
| jobMgr.endRule(rule); |
| } else synchronized (fDocumentAccessorLock) { |
| fDocumentLocked= false; |
| fDocumentAccessorLock.notifyAll(); |
| } |
| } |
| |
| /** |
| * Adds this as element state listener in the UI thread as it can otherwise |
| * conflict with other listener additions, since DocumentProvider is not |
| * thread-safe. |
| * |
| * @param editor the editor to get the display from |
| * @param provider the document provider to register as element state listener |
| */ |
| private void addElementStateListener(ITextEditor editor, final IDocumentProvider provider) { |
| // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=66686 and |
| // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=56871 |
| |
| Runnable runnable= () -> { |
| synchronized (fLock) { |
| if (fDocumentProvider == provider) |
| // addElementStateListener adds at most once - no problem to call repeatedly |
| provider.addElementStateListener(LastSaveReferenceProvider.this); |
| } |
| }; |
| |
| Display display= null; |
| if (editor != null) { |
| IWorkbenchPartSite site= editor.getSite(); |
| if (site != null) { |
| IWorkbenchWindow window= site.getWorkbenchWindow(); |
| if (window != null) { |
| Shell shell= window.getShell(); |
| if (shell != null) |
| display= shell.getDisplay(); |
| } |
| } |
| } |
| |
| if (display != null && !display.isDisposed()) |
| display.asyncExec(runnable); |
| else |
| runnable.run(); |
| } |
| |
| /** |
| * Initializes the given document with the given stream using the given |
| * encoding. |
| * |
| * @param document the document to be initialized |
| * @param storage the storage which delivers the document content |
| * @param encoding the character encoding for reading the given stream |
| * @param monitor a progress monitor for cancellation, or <code>null</code> |
| * @param skipUTF8BOM whether to skip three bytes before reading the stream |
| * @exception CoreException if the given storage can not be accessed or read |
| */ |
| private static void setDocumentContent(IDocument document, IStorage storage, String encoding, IProgressMonitor monitor, boolean skipUTF8BOM) throws CoreException { |
| Reader in= null; |
| InputStream contentStream= storage.getContents(); |
| try { |
| |
| if (skipUTF8BOM) { |
| for (int i= 0; i < 3; i++) |
| if (contentStream.read() == -1) { |
| throw new IOException(QuickDiffMessages.getString("LastSaveReferenceProvider.LastSaveReferenceProvider.error.notEnoughBytesForBOM")); //$NON-NLS-1$ |
| } |
| } |
| |
| final int DEFAULT_FILE_SIZE= 15 * 1024; |
| |
| if (encoding == null) |
| in= new BufferedReader(new InputStreamReader(contentStream), DEFAULT_FILE_SIZE); |
| else |
| in= new BufferedReader(new InputStreamReader(contentStream, encoding), DEFAULT_FILE_SIZE); |
| StringBuilder buffer= new StringBuilder(DEFAULT_FILE_SIZE); |
| char[] readBuffer= new char[2048]; |
| int n= in.read(readBuffer); |
| while (n > 0) { |
| if (monitor != null && monitor.isCanceled()) |
| return; |
| |
| buffer.append(readBuffer, 0, n); |
| n= in.read(readBuffer); |
| } |
| |
| document.set(buffer.toString()); |
| |
| } catch (IOException x) { |
| throw new CoreException(new Status(IStatus.ERROR, EditorsUI.PLUGIN_ID, IStatus.OK, "Failed to access or read underlying storage", x)); //$NON-NLS-1$ |
| } finally { |
| try { |
| if (in != null) |
| in.close(); |
| else |
| contentStream.close(); |
| } catch (IOException x) { |
| // ignore |
| } |
| } |
| } |
| |
| /** |
| * Returns <code>true</code> if the <code>encoding</code> is UTF-8 and |
| * the file contains a BOM. Taken from ResourceTextFileBuffer.java. |
| * |
| * <p> |
| * XXX: |
| * This is a workaround for a corresponding bug in Java readers and writer, |
| * see http://developer.java.sun.com/developer/bugParade/bugs/4508058.html |
| * </p> |
| * @param encoding the encoding |
| * @param storage the storage |
| * @return <code>true</code> if <code>storage</code> is a UTF-8-encoded file |
| * that has an UTF-8 BOM |
| * @throws CoreException if |
| * - reading of file's content description fails |
| * - byte order mark is not valid for UTF-8 |
| */ |
| private static boolean isUTF8BOM(String encoding, IStorage storage) throws CoreException { |
| if (storage instanceof IFile && StandardCharsets.UTF_8.name().equals(encoding)) { |
| IFile file= (IFile) storage; |
| IContentDescription description= file.getContentDescription(); |
| if (description != null) { |
| byte[] bom= (byte[]) description.getProperty(IContentDescription.BYTE_ORDER_MARK); |
| if (bom != null) { |
| if (bom != IContentDescription.BOM_UTF_8) |
| throw new CoreException(new Status(IStatus.ERROR, EditorsUI.PLUGIN_ID, IStatus.OK, QuickDiffMessages.getString("LastSaveReferenceProvider.LastSaveReferenceProvider.error.wrongByteOrderMark"), null)); //$NON-NLS-1$ |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /* IElementStateListener implementation */ |
| |
| @Override |
| public void elementDirtyStateChanged(Object element, boolean isDirty) { |
| if (!isDirty && element == fEditorInput) { |
| // document has been saved or reverted - recreate reference |
| new ReadJob().schedule(); |
| } |
| } |
| |
| @Override |
| public void elementContentAboutToBeReplaced(Object element) { |
| } |
| |
| @Override |
| public void elementContentReplaced(Object element) { |
| if (element == fEditorInput) { |
| // document has been reverted or replaced |
| new ReadJob().schedule(); |
| } |
| } |
| |
| @Override |
| public void elementDeleted(Object element) { |
| } |
| |
| @Override |
| public void elementMoved(Object originalElement, Object movedElement) { |
| } |
| } |