blob: 66ecb307e708e49b6883162574f1fefd4f07f88f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2012 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
* James Blackburn <jamesblackburn+eclipse@gmail.com> - [implementation] FileStoreTextFileBuffer eats IOException on external file save - https://bugs.eclipse.org/333660
*******************************************************************************/
package org.eclipse.core.internal.filebuffers;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.SequenceInputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnmappableCharacterException;
import java.nio.charset.UnsupportedCharsetException;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.Assert;
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.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.core.filebuffers.IFileBufferStatusCodes;
import org.eclipse.core.filebuffers.IPersistableAnnotationModel;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.source.IAnnotationModel;
/**
* @since 3.3 (previously available as JavaTextFileBuffer since 3.3)
*/
public class FileStoreTextFileBuffer extends FileStoreFileBuffer implements ITextFileBuffer {
private class DocumentListener implements IDocumentListener {
/*
* @see org.eclipse.jface.text.IDocumentListener#documentAboutToBeChanged(org.eclipse.jface.text.DocumentEvent)
*/
public void documentAboutToBeChanged(DocumentEvent event) {
}
/*
* @see org.eclipse.jface.text.IDocumentListener#documentChanged(org.eclipse.jface.text.DocumentEvent)
*/
public void documentChanged(DocumentEvent event) {
fCanBeSaved= true;
removeFileBufferContentListeners();
fManager.fireDirtyStateChanged(FileStoreTextFileBuffer.this, fCanBeSaved);
}
}
/**
* Reader chunk size.
*/
private static final int READER_CHUNK_SIZE= 2048;
/**
* Buffer size.
*/
private static final int BUFFER_SIZE= 8 * READER_CHUNK_SIZE;
/**
* Constant for representing the error status. This is considered a value object.
*/
private static final IStatus STATUS_ERROR= new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, IStatus.OK, FileBuffersMessages.FileBuffer_status_error, null);
/**
* Constant denoting UTF-8 encoding.
*/
private static final String CHARSET_UTF_8= "UTF-8"; //$NON-NLS-1$
/**
* Constant denoting an empty set of properties
* @since 3.1
*/
private static final QualifiedName[] NO_PROPERTIES= new QualifiedName[0];
/** The element's document */
protected IDocument fDocument;
/** The encoding used to create the document from the storage or <code>null</code> for workbench encoding. */
protected String fEncoding;
/** Internal document listener */
protected IDocumentListener fDocumentListener= new DocumentListener();
/** The encoding which has explicitly been set on the file. */
private String fExplicitEncoding;
/** Tells whether the file on disk has a BOM. */
private boolean fHasBOM;
/** The annotation model of this file buffer */
private IAnnotationModel fAnnotationModel;
/**
* Lock for lazy creation of annotation model.
* @since 3.2
*/
private final Object fAnnotationModelCreationLock= new Object();
/**
* Tells whether the cache is up to date.
* @since 3.2
*/
private boolean fIsCacheUpdated= false;
public FileStoreTextFileBuffer(TextFileBufferManager manager) {
super(manager);
}
/*
* @see org.eclipse.core.filebuffers.ITextFileBuffer#getDocument()
*/
public IDocument getDocument() {
return fDocument;
}
/*
* @see org.eclipse.core.filebuffers.ITextFileBuffer#getAnnotationModel()
*/
public IAnnotationModel getAnnotationModel() {
synchronized (fAnnotationModelCreationLock) {
if (fAnnotationModel == null && !isDisconnected()) {
fAnnotationModel= fManager.createAnnotationModel(getLocationOrName(), LocationKind.LOCATION);
if (fAnnotationModel != null)
fAnnotationModel.connect(fDocument);
}
}
return fAnnotationModel;
}
/*
* @see org.eclipse.core.filebuffers.ITextFileBuffer#getEncoding()
*/
public String getEncoding() {
if (!fIsCacheUpdated)
cacheEncodingState();
return fEncoding;
}
/*
* @see org.eclipse.core.filebuffers.ITextFileBuffer#setEncoding(java.lang.String)
*/
public void setEncoding(String encoding) {
fExplicitEncoding= encoding;
if (encoding == null || encoding.equals(fEncoding))
fIsCacheUpdated= false;
else {
fEncoding= encoding;
fHasBOM= false;
}
}
/*
* @see org.eclipse.core.filebuffers.ITextFileBuffer#getStatus()
*/
public IStatus getStatus() {
if (!isDisconnected()) {
if (fStatus != null)
return fStatus;
return (fDocument == null ? STATUS_ERROR : Status.OK_STATUS);
}
return STATUS_ERROR;
}
private InputStream getFileContents(IFileStore fileStore) throws CoreException {
if (!fFileStore.fetchInfo().exists())
return null;
return fileStore.openInputStream(EFS.NONE, null);
}
private void setFileContents(InputStream stream, IProgressMonitor monitor) throws CoreException {
OutputStream out= fFileStore.openOutputStream(EFS.NONE, null);
try {
byte[] buffer= new byte[8192];
while (true) {
int bytesRead= -1;
bytesRead= stream.read(buffer);
if (bytesRead == -1) {
out.close();
break;
}
out.write(buffer, 0, bytesRead);
if (monitor != null)
monitor.worked(1);
}
} catch (IOException ex) {
String message= (ex.getMessage() != null ? ex.getMessage() : ""); //$NON-NLS-1$
IStatus s= new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, IStatus.OK, message, ex);
throw new CoreException(s);
} finally {
try {
stream.close();
} catch (IOException e) {
} finally {
try {
out.close();
} catch (IOException e) {
}
}
}
}
/*
* @see org.eclipse.core.filebuffers.IFileBuffer#revert(org.eclipse.core.runtime.IProgressMonitor)
*/
public void revert(IProgressMonitor monitor) throws CoreException {
if (isDisconnected())
return;
IDocument original= null;
fStatus= null;
try {
original= fManager.createEmptyDocument(getLocationOrName(), LocationKind.LOCATION);
cacheEncodingState();
setDocumentContent(original, fFileStore, fEncoding, fHasBOM, monitor);
} catch (CoreException x) {
fStatus= x.getStatus();
}
if (original == null)
return;
String originalContents= original.get();
boolean replaceContents= !originalContents.equals(fDocument.get());
if (!replaceContents && !fCanBeSaved)
return;
fManager.fireStateChanging(this);
try {
if (replaceContents) {
fManager.fireBufferContentAboutToBeReplaced(this);
fDocument.set(original.get());
}
boolean fireDirtyStateChanged= fCanBeSaved;
if (fCanBeSaved) {
fCanBeSaved= false;
addFileBufferContentListeners();
}
if (replaceContents)
fManager.fireBufferContentReplaced(this);
IFileInfo info= fFileStore.fetchInfo();
if (info.exists())
fSynchronizationStamp= fFileStore.fetchInfo().getLastModified();
if (fAnnotationModel instanceof IPersistableAnnotationModel) {
IPersistableAnnotationModel persistableModel= (IPersistableAnnotationModel) fAnnotationModel;
try {
persistableModel.revert(fDocument);
} catch (CoreException x) {
fStatus= x.getStatus();
}
}
if (fireDirtyStateChanged)
fManager.fireDirtyStateChanged(this, fCanBeSaved);
} catch (RuntimeException x) {
fManager.fireStateChangeFailed(this);
throw x;
}
}
/*
* @see org.eclipse.core.filebuffers.IFileBuffer#getContentType()
* @since 3.1
*/
public IContentType getContentType () throws CoreException {
InputStream stream= null;
try {
if (isDirty()) {
Reader reader= new DocumentReader(getDocument());
try {
IContentDescription desc= Platform.getContentTypeManager().getDescriptionFor(reader, fFileStore.getName(), NO_PROPERTIES);
if (desc != null && desc.getContentType() != null)
return desc.getContentType();
} finally {
try {
reader.close();
} catch (IOException ex) {
}
}
}
stream= fFileStore.openInputStream(EFS.NONE, null);
IContentDescription desc= Platform.getContentTypeManager().getDescriptionFor(stream, fFileStore.getName(), NO_PROPERTIES);
if (desc != null && desc.getContentType() != null)
return desc.getContentType();
return null;
} catch (IOException x) {
throw new CoreException(new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, IStatus.OK, NLSUtility.format(FileBuffersMessages.FileBuffer_error_queryContentDescription, fFileStore.toString()), x));
} finally {
try {
if (stream != null)
stream.close();
} catch (IOException x) {
}
}
}
/*
* @see org.eclipse.core.internal.filebuffers.FileBuffer#addFileBufferContentListeners()
*/
protected void addFileBufferContentListeners() {
if (fDocument != null)
fDocument.addDocumentListener(fDocumentListener);
}
/*
* @see org.eclipse.core.internal.filebuffers.FileBuffer#removeFileBufferContentListeners()
*/
protected void removeFileBufferContentListeners() {
if (fDocument != null)
fDocument.removeDocumentListener(fDocumentListener);
}
/*
* @see org.eclipse.core.internal.filebuffers.FileBuffer#initializeFileBufferContent(org.eclipse.core.runtime.IProgressMonitor)
*/
protected void initializeFileBufferContent(IProgressMonitor monitor) throws CoreException {
try {
fDocument= fManager.createEmptyDocument(getLocationOrName(), LocationKind.LOCATION);
cacheEncodingState();
setDocumentContent(fDocument, fFileStore, fEncoding, fHasBOM, monitor);
} catch (CoreException x) {
fDocument= fManager.createEmptyDocument(getLocationOrName(), LocationKind.LOCATION);
fStatus= x.getStatus();
}
}
/*
* @see org.eclipse.core.internal.filebuffers.ResourceFileBuffer#connected()
*/
protected void connected() {
super.connected();
if (fAnnotationModel != null)
fAnnotationModel.connect(fDocument);
}
/*
* @see org.eclipse.core.internal.filebuffers.ResourceFileBuffer#disconnected()
*/
protected void disconnected() {
if (fAnnotationModel != null)
fAnnotationModel.disconnect(fDocument);
super.disconnected();
}
protected void cacheEncodingState() {
fEncoding= fExplicitEncoding;
fHasBOM= false;
fIsCacheUpdated= true;
InputStream stream= null;
try {
stream= getFileContents(fFileStore);
if (stream == null)
return;
QualifiedName[] options= new QualifiedName[] { IContentDescription.CHARSET, IContentDescription.BYTE_ORDER_MARK };
IContentDescription description= Platform.getContentTypeManager().getDescriptionFor(stream, fFileStore.getName(), options);
if (description != null) {
fHasBOM= description.getProperty(IContentDescription.BYTE_ORDER_MARK) != null;
if (fEncoding == null)
fEncoding= description.getCharset();
}
} catch (CoreException e) {
// do nothing
} catch (IOException e) {
// do nothing
} finally {
try {
if (stream != null)
stream.close();
} catch (IOException ex) {
FileBuffersPlugin.getDefault().getLog().log(new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, IStatus.OK, FileBuffersMessages.JavaTextFileBuffer_error_closeStream, ex));
}
}
// Use global default
if (fEncoding == null)
fEncoding= fManager.getDefaultEncoding();
}
/*
* @see org.eclipse.core.internal.filebuffers.FileBuffer#commitFileBufferContent(org.eclipse.core.runtime.IProgressMonitor, boolean)
*/
protected void commitFileBufferContent(IProgressMonitor monitor, boolean overwrite) throws CoreException {
String encoding= computeEncoding();
Charset charset;
try {
charset= Charset.forName(encoding);
} catch (UnsupportedCharsetException ex) {
String message= NLSUtility.format(FileBuffersMessages.ResourceTextFileBuffer_error_unsupported_encoding_message_arg, encoding);
IStatus s= new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, IStatus.OK, message, ex);
throw new CoreException(s);
} catch (IllegalCharsetNameException ex) {
String message= NLSUtility.format(FileBuffersMessages.ResourceTextFileBuffer_error_illegal_encoding_message_arg, encoding);
IStatus s= new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, IStatus.OK, message, ex);
throw new CoreException(s);
}
CharsetEncoder encoder= charset.newEncoder();
encoder.onMalformedInput(CodingErrorAction.REPLACE);
encoder.onUnmappableCharacter(CodingErrorAction.REPORT);
byte[] bytes;
int bytesLength;
try {
ByteBuffer byteBuffer= encoder.encode(CharBuffer.wrap(fDocument.get()));
bytesLength= byteBuffer.limit();
if (byteBuffer.hasArray())
bytes= byteBuffer.array();
else {
bytes= new byte[bytesLength];
byteBuffer.get(bytes);
}
} catch (CharacterCodingException ex) {
Assert.isTrue(ex instanceof UnmappableCharacterException);
String message= NLSUtility.format(FileBuffersMessages.ResourceTextFileBuffer_error_charset_mapping_failed_message_arg, encoding);
IStatus s= new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, IFileBufferStatusCodes.CHARSET_MAPPING_FAILED, message, null);
throw new CoreException(s);
}
IFileInfo fileInfo= fFileStore.fetchInfo();
if (fileInfo != null && fileInfo.exists()) {
if (!overwrite)
checkSynchronizationState();
InputStream stream= new ByteArrayInputStream(bytes, 0, bytesLength);
/*
* 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
*/
if (fHasBOM && CHARSET_UTF_8.equals(encoding))
stream= new SequenceInputStream(new ByteArrayInputStream(IContentDescription.BOM_UTF_8), stream);
// here the file synchronizer should actually be removed and afterwards added again. However,
// we are already inside an operation, so the delta is sent AFTER we have added the listener
setFileContents(stream, monitor);
// set synchronization stamp to know whether the file synchronizer must become active
fSynchronizationStamp= fFileStore.fetchInfo().getLastModified();
if (fAnnotationModel instanceof IPersistableAnnotationModel) {
IPersistableAnnotationModel persistableModel= (IPersistableAnnotationModel) fAnnotationModel;
persistableModel.commit(fDocument);
}
} else {
fFileStore.getParent().mkdir(EFS.NONE, null);
OutputStream out= fFileStore.openOutputStream(EFS.NONE, null);
try {
/*
* 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
*/
if (fHasBOM && CHARSET_UTF_8.equals(encoding))
out.write(IContentDescription.BOM_UTF_8);
out.write(bytes, 0, bytesLength);
out.flush();
out.close();
} catch (IOException x) {
IStatus s= new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, IStatus.OK, x.getLocalizedMessage(), x);
throw new CoreException(s);
} finally {
try {
out.close();
} catch (IOException x) {
}
}
// set synchronization stamp to know whether the file synchronizer must become active
fSynchronizationStamp= fFileStore.fetchInfo().getLastModified();
}
}
private String computeEncoding() {
// Make sure cache is up to date
if (!fIsCacheUpdated)
cacheEncodingState();
// User-defined encoding has first priority
if (fExplicitEncoding != null)
return fExplicitEncoding;
// Probe content
Reader reader= new DocumentReader(fDocument);
try {
QualifiedName[] options= new QualifiedName[] { IContentDescription.CHARSET, IContentDescription.BYTE_ORDER_MARK };
IContentDescription description= Platform.getContentTypeManager().getDescriptionFor(reader, fFileStore.getName(), options);
if (description != null) {
String encoding= description.getCharset();
if (encoding != null)
return encoding;
}
} catch (IOException ex) {
// Try next strategy
} finally {
try {
reader.close();
} catch (IOException x) {
}
}
// Use file's encoding if the file has a BOM
if (fHasBOM)
return fEncoding;
// Use global default
return fManager.getDefaultEncoding();
}
/**
* Initializes the given document with the given file's content using the given encoding.
*
* @param document the document to be initialized
* @param file the file which delivers the document content
* @param encoding the character encoding for reading the given stream
* @param hasBOM tell whether the given file has a BOM
* @param monitor the progress monitor
* @exception CoreException if the given stream can not be read
*/
private void setDocumentContent(IDocument document, IFileStore file, String encoding, boolean hasBOM, IProgressMonitor monitor) throws CoreException {
InputStream contentStream= getFileContents(file);
if (contentStream == null)
return;
Reader in= null;
try {
if (encoding == null)
encoding= fManager.getDefaultEncoding();
/*
* 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
*/
if (hasBOM && CHARSET_UTF_8.equals(encoding)) {
int n= 0;
do {
int bytes= contentStream.read(new byte[IContentDescription.BOM_UTF_8.length]);
if (bytes == -1)
throw new IOException();
n += bytes;
} while (n < IContentDescription.BOM_UTF_8.length);
}
in= new BufferedReader(new InputStreamReader(contentStream, encoding), BUFFER_SIZE);
StringBuffer buffer= new StringBuffer(BUFFER_SIZE);
char[] readBuffer= new char[READER_CHUNK_SIZE];
int n= in.read(readBuffer);
while (n > 0) {
buffer.append(readBuffer, 0, n);
n= in.read(readBuffer);
}
document.set(buffer.toString());
} catch (IOException x) {
String msg= x.getMessage() == null ? "" : x.getMessage(); //$NON-NLS-1$
IStatus s= new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, IStatus.OK, msg, x);
throw new CoreException(s);
} finally {
try {
if (in != null)
in.close();
else
contentStream.close();
} catch (IOException x) {
}
}
}
/**
* Checks whether the given file is synchronized with the local file system.
* If the file has been changed, a <code>CoreException</code> is thrown.
*
* @exception CoreException if file has been changed on the file system
*/
private void checkSynchronizationState() throws CoreException {
if (!isSynchronized()) {
Status status= new Status(IStatus.ERROR, FileBuffersPlugin.PLUGIN_ID, 274 /* IResourceStatus.OUT_OF_SYNC_LOCAL */, FileBuffersMessages.FileBuffer_error_outOfSync, null);
throw new CoreException(status);
}
}
/**
* Returns the location if it is <code>null</code> or
* the name as <code>IPath</code> otherwise.
*
* @return a non-null <code>IPath</code>
* @since 3.3.1
*/
private IPath getLocationOrName() {
IPath path= getLocation();
if (path == null)
path= new Path(fFileStore.getName());
return path;
}
}